[jsexpert] Angular Трансформация - Part 2

RXJS

Что такое реактивное программирование?

Реактивное программирование – это программирование, ориентированное на взаимодействие с асинхронными потоками данных (streams) и распространении изменений данных в потоках.

Можно выделить несколько особенностей реактивного программирования:

  • — ориентированно на взаимодействие с асинхронными потоками данных
  • — потоки имеют возможность передавать другие потоки
  • — данные, передающиеся по потоку слушаются подписчиками
  • — подписчики реагируют на изменение данных

Библиотека RxJS

RxJS — библиотека, которая реализует парадигму реактивного программирования в JavaScript.
RxJS является подмножеством целого ряда библиотек для реактивного программирования ReactiveX (RxJava, RxPython, Rx.NET, …), созданного разработчиками компании Microsoft и распространяющегося как свободное программное обеспечение.
RxJS включена в фреймворк Angular, как неотъемлемая его часть.
Некоторые встроенные классы Angular (HttpClient, Router.events, FormControl) наследуют методы класса Observable .

Observable

Observable — означает объект, который можно наблюдать, наблюдаемый .

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

Библиотека RxJS — состоит в основном из методов для работы с потоками.

Создать поток из класса Observable можно как с помощью конструктора (new), так и через метод Observable.create.

import {Observable} from 'rxjs';

// const observable = new Observable (function (observer) {  
const observable = Observable.create(function (observer) {
  // принимает как аргумент наблюдателя (observer)
  observer.next('Hello’);  // синхронная передача данных в поток
  setTimeout(() => {
    observer.next('World’); // асинхронная передача данных в поток
    observer.complete(); // завершение потока
  }, 2 * 1000);
});

Подписка на поток производится методом subscribe().
Метод subscribe() принимает три функции обратного вызова: next(), error(), complete() и возвращает подписку Subscription.

Без подписки поток не будет создан, таким образом библиотека RxJS оптимизирует создание потоков.

const subscription = observable.subscribe(
  word => console.log(word), // при next()
  error => console.log(error), // при ошибке
  () => console.log('Complete.') // при завершение
);

Observable. Методы создания

of()

of() — это упрощенный способ создания потока.
Принимает в качестве аргументов значение или несколько значений.

  1. const observable = of(1, 2, 'Hello’);
from()

Принимает итерируемые или способные преобразоваться в итерируемые объекты, а также Promise или другие Observable.

  1. const observable = from([1, 2, 3, 4, 5]);
fromEvent()

Позволяет создавать поток из событий на DOM-элементах. Принимает первым аргументом нативный DOM-элемент, а вторым название события в виде строки.

  1. const observable = fromEvent(document.querySelector(‘button’), 'click’);
interval()

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

  1. const observable = interval(1000);
range()

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

  1. const observable = range(3, 4) // 3, 4, 5, 6

Взаимодействие с потоками. Pipeable Operators

Для взаимодействия с потоками существует метод pipe().
В качестве параметров метод pipe() принимает так называемые pipeable-операторы.
Pipeable-операторы это такие функции, которые принимают на вход поток и возвращают поток, при этом внутри могут производить необходимые операции со значениями потока.
Импортируются pipeable-операторы из специальной директории rxjs/operators.

import {/* ops… */} from 'rxjs/operators';

observable
  .pipe(/*
    op1(),
    op2()
    ...
        */
  )
map()

Оператор map() модифицирует данные перед подпиской.
Аналог Array.prototype.map().
Принимает в качестве аргумента функцию, которая, в свою очередь, принимает очередное значения потока и возвращает модифицированное значение.

observable
  .pipe(
    map(x => {
      return x * x; // модификация значения
    })
  ).subscribe(/* колбэк подписки */);
pluck()

Оператор pluck() принимает в качестве аргумента строки, которые используются как ключи к свойствам объекта и позволяют доставать из очередного значения потока значения по этим ключам.
Возвращает новый поток с новыми значениями, полученными по ключам.

const observable = from([
  {id: 1, user: {name: 'Alex', lname: 'Smith'}},
  {id: 2, user: {name: 'John', lname: 'Doe'}}
]);
observable
  .pipe(
    // map(element => element.user.name),
    pluck('user', 'name'),
    map(name => name.toString().toUpperCase())
  ).subscribe(/* колбэк подписки */);
findIndex()

Оператор findIndex() ищет индекс элемента, который соответствует условиям в передаваемой функции. Возвращает результат первого вхождения и завершает поток.

from(['Hello', 'World', 3, 4, 5])
  .pipe(findIndex(x => x === 'World’))
  .subscribe(x => console.log(x)); // 1
find()

Оператор find() ищет элемент, который соответствует условиям в передаваемой функции. Возвращает результат первого вхождения и завершает поток.

from(['Hello', 'World', 3, 4, 5])
  .pipe(find(x => x === 'World’))
  .subscribe(x => console.log(x)); // World
filter()

Оператор filter() модифицирует данные перед подпиской.
Аналог Array.prototype.filter().
Принимает в качестве аргумента функцию-предикат, которая, в свою очередь, принимает очередное значения потока и возвращает новый поток значений, которые соответствуют заданному в функции условию.

const observable = range(1, 7)
  .pipe(filter(x => x % 2 === 0))
  .subscribe(x => console.log(x)); // 2, 4, 6

Более подробный пример по Pipeable Operators можете посмотреть по ссылке.

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

defaultIfEmpty()

Оператор defaultIfEmpty() принимает в качестве аргумента значение, которое будет передано в поток, если поток завершиться без создания каких-либо значений.

of() // empty value
  .pipe(defaultIfEmpty('and now not empty’))
  .subscribe(x => console.log(x)); // and now not empty
tap()

Оператор tap() позволяет провести побочное действие с очередными значениями потока, без изменения оригинального потока.
Принимает функцию в качестве аргумента. Возвращает оригинальный поток.


from([1, 2, 3])
  .pipe(tap(x => console.log(x * x))) // 1, 4, 9 - side effect
  .subscribe(x => console.log(x)); // 1, 2, 3 - original value
take()

Оператор take() позволяет получить ограниченное количество очередных значений потока.
Принимает в качестве аргумента количество значений.

from([1, 2, 3, 4, 5, 6, 7])
  .pipe(take(3))
  .subscribe(x => console.log(x)); // 1, 2, 3

Несколько различных примеров создания потоков и взаимодействия с ними можно посмотреть в обобщенном примере.

Совмещение потоков

merge()

Функция merge() принимает в качестве аргументов несколько потоков, возвращает общий поток, который содержит все элементы из входных потоков.
пример

concat()

Функция concat() принимает в качестве аргументов несколько потоков, возвращает общий поток, который вернет сначала все элементы из первого входного потока, а потом по порядку из следующих входных потоков.
пример

mergeAll()

Метод mergeAll() позволяет объединить значения всех вложенных потоков, из потока “родителя”, который их генерирует (потока высшего порядка).
пример

concatAll()

Метод concatAll() позволяет объединить значения всех сложенных потоков, из потока “родителя”, который их генерирует (потока высшего порядка), придерживаясь порядка очереди.
Все значения следующего сгенерированного потока будут выведены только после завершения предыдущего сгенерированного потока.
пример

mergeMap()

Метод mergeMap() объединяет в себе mergeAll() и map().
Позволяет объединить значения из двух (или нескольких) потоков.
Выполняет совместную бизнес-логику для очередных значений потоков немедленно, когда будут сгенерированы новые значения.
Избавляет от вложенных методов subscribe().
пример

concatMap()

Метод concatMap() объединяет в себе concatAll() и map().
Позволяет объединить значения из двух (или нескольких) потоков.
Выполняет совместную бизнес-логику для очередных значений потоков после завершения очереди операций для предыдущих.
Избавляет от вложенных методов subscribe().
пример

Subject

Существует два типа Observable:
Холодные — создают новый независимый поток на каждую подписку.
Как правило, все создаваемые Observable холодные.
пример

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

Очень часто на практике возникает задача получать разделяемый (Горячий) поток данных из одного холодного Observable.
Например HttpClient.get() возвращает нам холодный Observable, это означает, что несколько подписок будут отправлять разные get() запросы.
Решить подобную проблему нам поможет Subject.
пример

Subject

Subject — это еще один способ создать поток в библиотеке RxJS.
Subject создаются с использованием конструктора new Subject():

  1. const source$ = new Subject();

Subject позволяет в любой момент времени добавлять данные в поток конструкцией .next():

source$.next('first value');
source$.next('second value');
// ...
BehaviorSubject

BehaviorSubject — класс, который наследует методы и свойства класса Subject, но позволяет задать в функции конструкторе инициализационное значение.

  1. const source$ = new BehaviorSubject('init value’);
ReplaySubject

ReplaySubject — класс, который наследует методы и свойства класса Subject.
Сохраняет в буфер значения потока. Размер буфера задается в функции конструкторе. Новая подписка немедленно получит содержимое буфера.

  1. const source$ = new ReplaySubject(2);
AsyncSubject

AsyncSubject – класс, который наследует методы и свойства класса Subject. Передает в поток только последнее значение и только после завершения.
Похож на Promise.
пример

  1. const source$ = new AsyncSubject();

RxJS и Angular

Некоторые встроенные классы Angular наследуют методы класса Observable:

  • — EventEmitter – наследует Observable и расширяет его, добавляя метод emit();
  • — HttpClient – возвращает экземпляры класса Observable из HTTP методов;
  • — Async pipe – автоматически подписывается на экземпляры Observable или на Promise и отображает результат асинхронной операции в компоненте;
  • — Router.events – возвращает события маршрутизации как экземпляр Observable;
  • — ActivateRoute.url – возвращает события изменения параметров маршрутизации как экземпляр Observable;
  • — Reactive forms FormControl – содержит поля valueChanges and statusChanges, которые наследуют поведение Observable.

Встроенные классы Angular умеют автоматически управлять памятью и завершать все созданные в жизненном цикле компонента потоки автоматически .
От всех созданных «кастомных» потоков необходимо отписываться в компоненте с помощью метода unsubscribe().
Самым подходящим местом для отписки будет хук жизненного цикла компонента ngOnDestroy. Для этого удобно помещать подписку в переменную subscription.

@Component({…})
export class AppComponent implements OnInit, OnDestroy {
  subscription: Subscription;
  ngOnInit() {
     this.subscription = interval(500).subscribe(x => console.log(x));
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();  // отписка!
  }
}

Поскольку вызов HTTP методов возвращает нам экземпляр класса Observable, удобно работать с ними, используя методы RxJS.

Очень часто возникает задача, когда нам необходимо получить данные из бэкенда, и на основе полученных данных сделать еще один HTTP запрос и сохранить конечный результат. Что бы избежать вложенных subscribe() нам может пригодится метод mergeMap().

@Component({…})
export class AppComponent {
  user$: Observable<{}>;
  constructor(private http: HttpClient) { }

  ngOnInit() {
    this.user$ = this.http.get('/api/people/1').pipe(
      mergeMap(user => this.http.get("/api/details/"+user.id))
    );
    this.user$.subscribe(data => console.log(data));
  }
}

Другим частым вариантом на практике есть такой «кейс», когда необходимо отправить одновременно несколько HTTP-запросов и продолжить работу с этими данными, кода все запросы получать ответ. Такое поведение может напоминать статический метод Promise.all().
Для такой задачи отлично подойдет функция forkJoin библиотеки RxJS.
Работу функции forkJoin можно посмотреть на примере

ДОМАШНЕЕ ЗАДАНИЕ

В этом домашнем задании пока не будет реализации нового функционала.
Заданием будет привести приложение к единообразному виду.

  1. В прошлом домашнем задании у нас была реализация дополнительного поиска. На текущем этапе необходимо будет его убрать. У нас будет всего три маршрута в приложении: главная страница, все фильмы и все актеры. Функциональность для навигации также оставить в тулбаре. Должно остаться только поле поиска, которое будет располагаться в тулбаре. Поиск соответственно, как и прошлом домашнем задании, должен осуществляется через внешний api. Функционал “пейджинга” должен работать, как и в прошлых реализациях.
  2. Страница “актеры” должна находится на отдельном роуте. Соответственно и на этой странице должен работать поиск через внешний api и пейджинг, как и на странице с фильмами. Обратите внимание, что компонент который используется для поиска, должен быть один и переиспользоваться в других компонентах.
  3. Третьим роутом в нашем приложении будет “главная” страница. На ней должна быть выведена общая информация из “фильмов” и из “актеров”. Соответствующие секции должны отделятся подзаголовками, под которыми будет располагаться несколько элементов из секций и под ними ссылка “смотреть больше…”, “кликая” на которую произойдет переход на соответственную страницу.

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

ROUTES

Что такое маршруты (роуты, Routes)

Router это механизм, который позволяет загружать/активировать разные компоненты Angular приложения в зависимости от того, что указано в адресной строке браузера.
Таким образом, для пользователя процесс работы с приложением выглядит как обычный переход по ссылкам.
Но вместо полной перегрузки страницы, как это происходит в обычных приложениях, роутер «подхватывает» изменение в адресной строке и переходит на другой компонент вместо перезагрузки экрана.
Работа с роутером не сложна и в целом интуитивно понятна.

Пошаговая настройка Роутера

  1. Установить специальный HTML элемент сразу после тега HEAD. Если ваше приложение находиться не в корневой папке сайта, соответственно вы пропишете не “/”, а путь к приложению.
<head>
    <base href="/">
</head>
  1. Подключить роутер модуль.
    Роутер не является частью Core модуля, по этому должен быть подключен отдельно.
import { RouterModule, Routes } from '@angular/router';
  1. Непосредственно настроить сами маршруты.
    Никаких маршрутов по умолчанию не существует. По этому вам необходимо произвести непосредственную конфигурацию маршрутов самостоятельно.
import { RouterModule, Routes } from '@angular/router';

const appRoutes: Routes = [
  { path: 'film-list', component: FilmListComponent },
  { path: 'film/:id',  component: FilmComponent },
  {
    path:     'dashboard',
    component: DashboardComponent,
    data: { title: 'Main page for film catalog App' }
  },
  {
    path:     'profile',
    component: UserProfileComponent,
    resolve:  [UserService]
  },
  { path: '',
    redirectTo: '/login',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // позволяет видеть отладочную информацию
    )
  ],
  ...
})
export class AppModule { }
  1. Добавить Router Outlet директиву.
    Необходимо указать место в шаблоне где непосредственно будет отображаться содержимое компонентов.
<h2>Вас приветствует приложение Film App</h2>
<span>Здесь вы сможете узнать многое про новинки кино</span>

<router-outlet></router-outlet>
  1. Прописать ссылки на соответствующие маршруты используя директиву “Router Link”
<h2>Вас приветствует приложение Film App</h2>
<span>Здесь вы сможете узнать многое про новинки кино</span>

<nav>
    <a routerLink="/dashboard" routerLinkActive="active">Главная страница</a>    
    <a routerLink="/profile" routerLinkActive="active">Настройки пользователя</a>
</nav>

<router-outlet></router-outlet>

Wild Card Route

Если пользователь пытается перейти на маршрут который не указан в конфигурации, вы можете указать специальный маршрут на который будут перенаправлены все такие запросы.
Такой маршрут называется “Wildcard route”

{ path: '**', component: PageNotFoundComponent }
// or
{ path: '**', component: Page404Component }

Порядок объявления маршрутов играет роль.
Если вы укажете следующую конфигурацию то по сути не сможете открыть ни один маршрут.

  { path: '**', component: Page404Component },
  {
    path:     'dashboard',
    component: DashboardComponent,
  },
  {
    path:     'profile',
    component: UserProfileComponent,
    resolve:  [UserService]
  }

Способы настройки модулей. Отдельный роутинг модуль.

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

  • прописать конфигурацию в root модуле (AppModule);
    • вынести конфигурацию в константу, затем использовать ее в методе RouterModule.forRoot();
  • сделать отдельный модуль для конфигурирования роутов;

Первый вариант допускается только в случае маленького приложения.
Последний вариант рекомендуется делать если у вас достаточно большое приложение и маршрутов много.

#1: Внутри app.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';


@NgModule({
  imports: [
    BrowserModule,
    DashboardModule,
    RouterModule.forRoot([
      { path: 'film-list', component: FilmListComponent },
      { path: 'dashboard',component: DashboardComponent},
      { path: '',
        redirectTo: '/login',
        pathMatch: 'full'
      },
      { path: '**', component: PageNotFoundComponent }
    ])
  ],
  declarations: []
})
export class AppModule { }

#2: Используем константу в app.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';


const routesConfig: Routes = [
  { path: 'film-list', component: FilmListComponent },
  { path: 'dashboard',component: DashboardComponent},
  { path: '',
    redirectTo: '/login',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];


@NgModule({
  imports: [
    BrowserModule,
    DashboardModule,
    RouterModule.forRoot(routesConfig)
  ],
  declarations: []
})
export class AppModule { }

#3: Отдельный модуль app.router.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const appRoutes: Routes = [
  { path: 'film-list', component: FilmListComponent },
  { path: 'dashboard',component: DashboardComponent},
  { path: '', redirectTo: '/login', pathMatch: 'full'},
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app.routing.module';

@NgModule({
  imports: [
    BrowserModule,
    DashboardModule,
    AppRoutingModule 
  ],
  declarations: []
})
export class AppModule { }

Additional Info

Статический метод RouterModule.forRoot можно вызывать только в корневом модуле (Root Module).

Во всех других модулях необходимо вызывать RouterModule.forChild.

Передача параметров и снятие значения

Для открытия конкретного элемента на просмотр или редактирование, необходимо передать еще один параметр в адресной строке.

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

const appRoutes: Routes = [
  { path: 'film-list', component: FilmListComponent },
  { path: 'film/:id',  component: FilmComponent }
];

// обработаны будут адреса вида 
// http://test.com/film/15
// http://test.com/film/225

Перейти на такой маршрут вы можете по ссылке в шаблоне или выполнив соответствующий метод.

 <a routerLink="/film/14587" routerLinkActive="active">Темная башня</a>

openFilmById(id) {
  this.router.navigate(['/film', id]);
}

Для получения параметров из URL используют специальный сервис ActivatedRoute. Существует два основных метода работы с ним:

  • получение данных динамически с помощью Observable
  • получение данных статически с помощью snapshot

Сервис ActivatedRoute содержит в себе информацию о текущем активном состоянии.

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

import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Component, OnInit } from "@angular/core";

@Component({
    selector: "film",
    templateUrl: "film.component.html"
})
export class FilmComponent implements OnInit {
    id: number;
    film : Film;

    constructor(
        private route: ActivatedRoute,
        private filmService: FilmService
    ) { }

    ngOnInit() {
        this.route.paramMap.subscribe((params: ParamMap) => {
            this.id = +params.get("id");
            this.filmService.getFilmById(this.id)    
                .then(result => this.film = result); 
        });
    }
}

Особенностью сервиса является то, что он непрерывно следит за обновлением адресной строки. Если вы поменяете id в url, то это обновление «подхватиться» и вы получите новый id в коде компонента без необходимости перегружать маршрут.
Если необходимости «следить» за параметрами нет, то вы можете использовать другой подход без Observable.

У router сервиса есть проперти snapshot.paramMap откуда вы можете получить параметры, которые были установлены на
момент загрузки маршрута.

import { Router, ParamMap } from '@angular/router';
import { Component, OnInit } from "@angular/core";

@Component({
    selector: "film",
    templateUrl: "film.component.html"
})
export class FilmComponent implements OnInit {
    id: number;
    film: Film;

    constructor(
        private route: ActivatedRoute,
        private filmService: FilmService
    ) { }

    ngOnInit() {

        let id = this.route.snapshot.paramMap.get('id');
        
        this.filmService.getFilmById(this.id)    
                .then(result => this.film = result); 
    }
}

Не обязательные параметры

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

// параметр id является обязательным
{ path: 'film/:id', component: FilmComponent }


//так же вы можете передать опциональные параметры
this.router.navigate(['/dashboard', { userName: "John", status: 'active' }]);

// в адресной строке будет отображено
http://test.com/dashboard;userName=John;status=active

// получить параметры можно таким же способом
this.activatedRoute.paramMap.subscribe((params: ParamMap) => {
    this.userName = +params.get("userName");
});

Дочерние маршруты

Когда приложение растет в размере, становиться сложно уместить все маршруты на одном уровне вложенности.
Для решения этой проблемы Angular предоставляет возможность создавать дочерние маршруты. Рассмотрим концепцию дочерних маршрутов на следующей схеме.


Конфигурация роутера для вложенных маршрутов выглядит приблизительно следующим образом.

/* возможные адреса
http://test.com/dashboard/
http://test.com/dashboard/details/John
http://test.com/dashboard/overview
*/

const aboutRoutes: Routes = [
  {
    path: '/dashboard',
    component: DashboardComponent,
    children: [
      {
        path: '',
        component: DashboardInnerComponent,
      },
      {
        path: 'details/:username',
        component: DashboardUserComponent
      },
      {
        path: 'overview',
        component: DashboardOverviewComponent,
      }
    ]
  }
];

Детальнее дочерние маршруты рассмотрим на реальном примере, ссылка на который будет в конце конспекта.

“Хранители маршрутов” или Router guards

В реальных приложениях часто возникает ситуация, когда необходимо управлять тем какой маршрут можно открыть, а какой нельзя.
Для этого реализован специальный механизм “router guards”.

Наиболее часто Router Guards применяются для таких задач:

  • — перед открытием маршрута, проверить имеет ли пользователь право его открывать (CanActivate, CanActivateChild)
  • — если пользователь например вводил данные в форму, а затем решил перейти на другой компонент, задать вопрос пользователю точно ли он хочет покинуть данный маршрут без сохранения информации (CanDeactivate)
  • — загружать маршрут только тогда, когда все данные уже получены (Resolve)

Другими словами роутер поддерживает следующие guard интерфейсы:

  • CanActivate – можем ли мы открыть определенный маршрут;
  • CanActivateChild – можем ли мы открывать дочерние маршруты, текущего маршрута
  • CanDeactivate – можем ли мы покинуть текущий маршрут
  • Resolve – получить данные до того как фактически загрузить маршрут

Для активации Router guard для конкретного маршрута необходимо сделать следующее:

  • — добавить соответствующий ключ в саму конфигурацию роутера;
  • — в качестве параметра указать специальный сервис.

В данном случае, AuthGuard сервис разрешает нам открыть AdminComponent если мы уже залогинились (возвращает true) или не делает ничего (возвращает false) если пользователь не авторизирован.

const adminRoutes: Routes = [{
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard]
}];

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(): boolean {
     if (this.authService.isLoggedIn()) { 
        return true; 
     } else {
        // это не обязательно, можно просто вернуть false и никуда не перебрасывать
        this.router.navigate(['/login']); 
        return false;
     }
  }
}

Подобным образом работает предварительная загрузка данных Resolve.

const adminRoutes: Routes = [{
    path: 'user/:id',
    component: UserComponent,
    resolve: {
        user: UserResolver 
    }
}];
/*------------------------------------*/

import { Injectable } from '@angular/core';
import { Router, Resolve, ActivatedRouteSnapshot } from '@angular/router';

@Injectable()
export class UserResolver implements Resolve {
  constructor(private userService: UserService,  private route: ActivatedRoute, private router: Router) {}
  
  resolve(route: ActivatedRouteSnapshot): Promise {
    
    this.route.paramMap.subscribe((params: ParamMap) => {
        this.id = +params.get("id");
        return this.userService.getUserbyId(id).subscribe(user => {
              if (user) {
                return user;
              } else { 
                this.router.navigate(['/user-list']);
                return null;
              }
        });
    });
  }
}

LazyLoad модули в Angular

Обычно мы собираем наши SPA в виде одного исходного JS-файла, так называемого «бандла». Но по мере роста количества «страниц» (компонент) и функциональности в приложении, размер такого «бандла» может стать критически большим (> 2MB). Пользователю приложения необходимо будет ждать полной загрузки, что бы начать использовать функционал.

Начиная с версии 5, в Angular появилась возможность «ленивой» (lazy) подгрузки частей (chunk-ов) приложения по востребованию, то есть когда осуществляется переход на соответствующий маршрут.

Такой механизм сильно сокращает время загрузки приложения, так как изначально загружается только необходимая часть приложения для отображения «стартовой» страницы.

Что бы внедрить механизм LazyLoad модулей, необходимо:

  • — для отдельных маршрутов создать отдельные модули со своими внутренними routing-модулями;
  • — не импортировать модули и компоненты из модулей других маршрутов;
  • — не импортировать lazy-модули в корневой модуль (напр. app.module.ts );
  • — подключить дочерние lazy-модули в корневом routing-модуле через loadChildren;
// app-routing.module.ts
{
  path: 'users',
  loadChildren: './users/users.module#UsersModule',
}

Несложно заметить, что решив проблему размера приложения с помощью механизма LazyLoad, мы создали новую. Теперь при первом переходе на маршруты lazy-модулей пользователю приложения придётся ждать завершения загрузки этого модуля.
Решить это можно изменением стратегии предзагрузки модулей.
По умолчанию установлена стратегия NoPreload, которая соответствует стандартному поведению, когда модули подгружаются при переходе на маршрут.
Также можно задать стратегию PreloadAllModules, при которой все lazy-модули будут догружены сразу же после загрузки и отображении модуля &laquo;стартовой&raquo; страницы.
Задается конфигурация стратегии предзагрузки модулей в статическом методе forRoot.

…
import {…, PreloadAllModules} from '@angular/router’;

@NgModule({
  imports: [RouterModule.forRoot(routes, {preloadingStrategy: PreloadAllModules})],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

Как и для обычных маршрутов, для маршрутов lazy-модулей так же можно указать Guards, которые задаются в конфигурации роута с помощью ключа canLoad и принимает в качестве настройки Guard, который должен имплементировать интерфейс CanLoad.
Стоит отметить, что для маршрутов защищенных с помощью Router Guards canLoad не будет работать стратегии предзагрузки PreloadAllModules.

Полный пример на тему маршрутизации, который был продемонстрирован на сессии, можете найти по ссылке.

  1. Вам необходимо сделать простейшее приложение со списком товаров.
    Дизайн и наполнение html шаблонов практического значения не имеет. Сделайте на свое усмотрение.
    Суть задания — работа с маршрутами у которых 2 обязательных параметра.

У вас есть сервис itemsService.
В приложении один родительский маршрут (itemsListComponent).
На экране отображается список товаров. Список товаров — это массив объектов с полями title, description и price.
Соответствующие данные пропишите (замокаете/захардкодите) в сервисе и с помощью метода getItems получите в компоненте.
В компоненте вы отобразите название и цену товара в виде списка. Описание не отображается.

Под каждым товаром 2 кнопки/ссылки. “Купить” и “Просмотреть”.
При нажатии на любую из кнопок открывается один и тот же маршрут(path: “user/:id/:status”)
Для кнопки “Просмотреть” параметр status равен “view”.
Для кнопки “Купить” параметр status равен “buy”.

То есть, в каждом из случаев вы открываете полное описание товара (данные из полей title, description и price).
Данные о конкретном товаре вы получите из сервиса с помощью метода getItemById.

Если была нажата кнопка “Купить” вы отобразите на экране сообщение для пользователя “Товар добавлен в корзину”.
Если была нажата кнопка “Просмотреть” вы просто открываете описание товара.
Обязательные параметры вы получите с помощью сервиса activatedRoute, как было показано на лекции.

Для реализации задачи, можете воспользоватся следующими данными.

ДОМАШНЕЕ ЗАДАНИЕ

В этом задании, Вам необходимо будет продолжить работу над приложением о фильмах.
Напомним, что на данном этапе, в нашем приложении есть три компонента:

  • — «главная» страница
  • — страница «популярные фильмы»
  • — страница «популярные актеры»
Задание:
  1. Реализовать функционал авторизации в приложении.
    Для этого необходимо создать новый компонент с формой для авторизации и соответственный сервис с функционалом авторизации.
    Авторизацию можно сделать двумя вариантами. В первом и более предпочтительном случае можете воспользоваться для авторизации внешним API https://reqres.in, пример работы с которым есть в приложении, которое прикреплено к сессии. Во втором, более простом, можете просто сравнивать введенные логин и пароль со статическим объектом в сервисе авторизации. Сохранять статус авторизации в localStorage.
    При новом входе в приложение, если пользователь авторизирован, будет отрабатывать переадресация по маршруту «/main» на «главную» страницу, а если нет, то на страницу авторизации.
    Также необходимо «защитить» от неавторизированного доступа маршруты «главной» страницы, страницы «популярные фильмы» и страницы «популярные актеры» с помощью Guards по прямой ссылке. Ссылки в тулбаре приложения на перечисленные страницы прятать от неавторизированного пользователя.

  2. Создать новые компоненты, которые будут отвечать за детальную информацию о фильме и о актере по соответственным маршрутам: «films/:filmId» и «actors/:actorId». Формат более детальной информации (описание, год выпуска, рейтинг, студия, постеры, …) выберете на свое усмотрение. Стоит учесть, что данные по этим ссылкам так же должны быть защищены от неавторизированного доступа по прямой ссылке с помощью Guards.

  3. Создать страницу-заглушку, которая будет отображаться при переходе по несуществующему в приложении маршруту. Отображать страницу-заглушку несколько секунд, после чего пользователя перенаправлять на «главную» страницу или страницу авторизации, в зависимости от статуса авторизации пользователя. На странице-заглушке отображать информацию с тем, что пользователь будет «перенаправлен» и обратный таймер, который будет отсчитывать время до переадресации.

4*. Необязательное задание. Компоненты с «главной» страницей, актерами и фильмами вынести в отдельный lazy-модуль. «Защитить» загрузку lazy-модуля с помощью Guards canLoad от неавторизированного доступа.

FORMS

Возможности Angular по работе с формами:

  • — двухстороннее связывание с использованием ngModel
  • — директива ngForm
  • — механизм для отслеживания текущего статуса формы
  • — автоматическое добавление классов, которые отображают этот статус
  • — использование templateReferenceVariable для манипуляции данными внутри шаблона
  • — FormGroup, FormControl, FormBuilde

Template Driven Forms

» Подход в котором большая часть работы с формой заключена в самом шаблоне.

Что бы начать работу с шаблонными формами необходимо выполнить несколько шагов:

  • — импортировать модуль FormsModule
  • — создать сам шаблон с формой и компонент, который будет ей управлять
  • — создать «модель» в которой вы будете хранить данные из формы
  • — «передать» значения полей в компонент с помощью ngModel
  • — прописать валидационные сообщения (если нужно)
  • — добавить стили для стандартных классов, которые меняются в процессе валидации
  • — воспользоваться Template Reference Variable (если это нужно)
  • — добавить обработку нажатия на Enter (если это нужно)
  • — добавить кнопку Submit/Save/Done/Clear
    <form (ngSubmit)="save()" #demoForm="ngForm">
      <div>
        <label for="name">User Name</label>
        <input type="text" id="name"
               required [(ngModel)]="model.userName" name="userName"
               #userNameRef="ngModel">
        <div [hidden]="userNameRef.valid || userNameRef.pristine"
             class="error-message">
          User Name is required
        </div>
      </div>

      <div>
        <label for="userDesc">User Desc</label>
        <input type="text" id="userDesc"
               [(ngModel)]="model.userDesc" name="userDesc">
      </div>


      <button type="submit" [disabled]="!demoForm.form.valid">Submit</button>
      <button type="button" (click)="demoForm.reset()">Reset</button>
    </form>

Template Reference Variable

Используется для того чтобы получить ссылку не на сам input элемент, а на директиву ngModel которая с ним связана.
Таким образом можно снимать диагностические параметры и определять состояние формы.

<input 
    type="text" 
    id="userName"
    required [(ngModel)]="model.name" name="userName"
    #nameRef="ngModel"
>
<div 
    [hidden]="nameRef.valid || nameRef.pristine"
    class="error-message"
>
    Укажите имя пользователя
</div>

Однако не стоит забывать и про другой сценарий работы с Template Reference Variable, который часто используется.

@Component({
  selector: 'update-field',
  template: `
    <input #nameInput
      (keyup.enter)="update(nameInput.value)"
      (blur)="update(nameInput.value)">
      <button (click)="update(nameInput.value)">Добавить значение</button>

    <p>Значение из инпута: {{value}}</p>
  `
})
export class UpdateFieldComponent {
  value = '';
  update(value: string) { this.value = value; }
}
Специальная директива NgForm

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

Так же содержит общий параметр валидна ли форма в целом или нет.

<form class="example" #demoForm="ngForm">
    <input></input>
    <button type="submit" (click)="submit()" [disabled]="!demoForm.form.valid">
</form>
<div *ngIf="!demoForm.form.valid">Форма заполнена некорректно</div>

Директива ngModel кроме двухстороннего связывания так же содержит механизм, который позволяет с помощью CSS стилей определить «прикасался» ли пользователь к полю, менялось ли значение и валидно ли поле.
К полю автоматически добавляются специальные css свойства.
Необходимо помнить, что использование атрибута name для input поля обязательно, если вы используете ngModel внутри формы.

Состояние поля Если «true» Если «false»
Фокус был в этом поле ng-touched ng-untouched
Содержимое менялось ng-dirty ng-pristine
Поле валидное ng-valid ng-invalid

Model Driven Forms (Reactive Forms)

» Подход в котором большая часть работы с формой заключена в js коде компонента.

Более «современный» подход, который позволяет разгрузить шаблон.
В отличии от Template Driven подхода практически вся «настройка» формы выполняются в коде компонента. Включая валидацию, установку дефолтных значений и т.д.
Фактически в компоненте создается специальный объект, который управляет формой. Вам только необходимо подключить его в шаблоне.
Суть подхода в следующем.

  • — создаете экземпляр класса FormGroup (1)
  • — привязываете его к форме (2)
  • — внутри FormGroup создаются FormControl (3)
  • — каждый FormControl привязывается к элементу формы по имени (4)

Простой пример реализации выглядит следующим образом.

export class UserSettingsComponent{         (1)
  userForm = new FormGroup ({
    userName: new FormControl(),            (3)
    userDescr: new FormControl()            (3)
  });}

<div>User Settings</div>
<form [formGroup]="userForm">               (2)
  <div>
    <label>Name:
      <input formControlName="userName">    (4)
    </label>
  </div>

  <div>
    <label>Description:
      <input formControlName="userDescr">   (4)
    </label>
  </div>
</form>

Основные возможности Reactive Forms:

  • — установка default значений (первый аргумент FormControl)
  • — массив валидаторов для каждого поля (второй аргумент FormControl)
  • — легкий доступ к данным (value)
  • — проверка на валидность формы (status)
  • — обновление значений (setValue, patchValue)
  • — сброс формы (reset)
export class UserSettingsComponent{         
  userForm = new FormGroup ({
    userName: new FormControl('John', Validators.minLength(4)),
    userDescr: new FormControl('', [Validators.required, Validators.minLength(4), Validators.maxLength(12)])            
  });

  update () {
    this.userForm.setValue({userName: 'Anakin', userDescr: 'Jedai'});
  }

  save() {
    console.log(this.userForm.value);   // {userName: 'Anakin', userDescr: 'Jedai'}
    console.log(this.userForm.status);  // 'VALID'
  }  
  makeClear () {
    this.userForm.reset();
  } 

}
  • — setValue — позволяет установить значения в поля формы. Модель данных должна точно совпадать. То есть нужно указывать все поля которые описаны в объекте
  • — patchValue — позволяет обновить только отдельные поля
  • — reset — очищает форму (все поля pristine, untouched, и значения null/набор значений по умолчанию)
export class UserSettingsComponent{         
  userForm = new FormGroup ({
    userName: new FormControl('John'),
    userDescr: new FormControl('', Validators.required)            
  });

  update () {
    this.userForm.setValue({userName: 'Anakin', userDescr: 'Jedai'});
    this.userForm.patValue({userDescr: 'test description'});
  }

  save() {
    console.log(this.userForm.value);   // {userName: 'Anakin', userDescr: 'test description'}
    console.log(this.userForm.status);  // 'VALID'
  }  
  makeClear () {
    this.userForm.reset({userName: 'John', userDescr: 'Connor'});
  } 

}

Базовым классом для FormControl, FormGroup и FormArray является класс AbstractControl. С полным списком свойств и полей которого, можно ознакомится в документации.

FormBuilder

Специальная синтаксическая конструкция, которая позволяет осуществлять точно такую же настройку, но сокращает запись с точки зрения синтаксиса.

  • — внедряете зависимость FormBuilder в конструктор
  • — создаете его экземпляр похожим образом как создавали FormGroup и FormControl
export class UserSettingsComponent{         
  userForm: FormGroup;
  constructor(private fb: FormBuilder) {  }

  ngOnInit(){
    this.buildForm();
  }

  buildForm() {
    this.userForm = this.fb.group({
      userName: ['John', [
        Validators.required,
        Validators.minLength(4),
        Validators.maxLength(15)
      ]],
      userEmail: ['',[
          Validators.required,
          Validators.pattern(/[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}/)
      ]]
    });
  }
}
Валидация

Существует 2 основных подхода к валидации.

  • — submit — только после нажатия кнопки submit/ok/save
  • — «on the fly» — на событие keyup, потеря фокуса

Как реализуются эти подходы к валидации в Model Driven Forms?
В первом случае вы осуществляете проверку непосредственно после вызова метода save(), submit() и т.д.
Во втором, вы подписываетесь на событие формы valueChanges, которое происходит при каждом изменении данных.
Как показывать сообщения?

Наиболее частый сценарий это HTML блоки (например div) которые показываются или прячутся (*ngIf) в зависимости от того существует определенной свойство в объекте с ошибками или нет.

export class UserSettingsComponent{         
  userForm: FormGroup;
  constructor(private fb: FormBuilder) {  }

  formErrors = {
    "userName": "",
    "userEmail": ""
  }
  validationMessages = {
    "userName": {
      "required": "Обязательное поле.",
      "minlength": "Значение должно быть не менее 4х символов.",
      "maxlength": "Значение не должно быть больше 15 символов."
    },
    "userEmail": {
      "required": "Обязательное поле.",
      "pattern": "Не правильный формат email адреса."
    }
  }

  ngOnInit(){
    this.buildForm();
  }

  buildForm() {
    this.userForm = this.fb.group({
      userName: ['John', [
        Validators.required,
        Validators.minLength(4),
        Validators.maxLength(15)
      ]],
      userEmail: ['',[
          Validators.required,
          Validators.pattern(/[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}/)
      ]]
    });
    this.userForm.valueChanges.subscribe(data => this.onValueChange());
  }


    onValueChange() {
        if (!this.userForm) return;

        for (let item in this.formErrors) {
            this.formErrors[item] = "";
            let control = this.userForm.get(item);

            if (control && control.dirty && !control.valid) {
                let message = this.validationMessages[item];
                for (let key in control.errors) {
                    this.formErrors[item] += message[key] + " ";
                }
            }
        }
    }

    onSubmit() {
        console.log("submitted");
        console.log(this.userForm.value);
    }
}
<div class="container">
    <h3>User Reactive Form</h3>
    
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
        <div>
            <label for="name">Имя</label>
            <input id="name" type="text" formControlName="userName"/>

            <div *ngIf="formErrors.userName" class="error">
            {{ formErrors.userName}}
            </div>
        </div>

        <div>
            <label for="name">Email</label>
            <input id="email" type="text" formControlName="userEmail"/>

            <div *ngIf="formErrors.userEmail" class="error">
                {{ formErrors.userEmail}}
            </div>
        </div>

        <button type="submit" [disabled]="!userForm.valid">Submit</button>
    </form>
</div>

С полным списком встроенных валидатовов можете ознакомится в документации.

Полный пример на тему «Forms», который был продемонстрирован на сессии, можете найти поссылке.

  1. Сделать валидацию профиля пользователя используя только model driven подход.
    У вас должна быть форма в которую входят
    Имя: буквы и числа, длина от 2 до 25
    Фамилия: буквы и числа, длина от 2 до 25
    Телефон: соответствует шаблону +380(***) *** ** **
    Адрес (textarea): буквы и числа, длина от 2 до 200
    Email: email
    Задачу выполнить в stackblitz.

  2. Необходимо сделать форму, которая проверяет возраст пользователя. Для валидации используйте любой подход (Template Driven или Model Driven).
    Изначально в форме виден один селектбокс: «какой жанр фильма вы хотите посмотреть» (мультфильм, комедия, ужасы) .
    Если пользователь выбирает «ужасы» появляется input поле с лейбой: «Сколько тебе лет».
    В случае если пользователь указал меньше 18, форма прячется и появляется надпись «Вы не можете просматривать этот контент».
    В случае если пользователь указал 18 или больше, появляется селектбокс с лейбой: «Что такое VHS». Варианты ответа «Видеокасета», «Фреймворк».
    В случае если пользователь выбрал «Видеокасета» форма прячется и отображается надпись «Вы прошли проверку»
    В случае если пользователь выбрал «Фреймворк» форма прячется и отображается надпись «Ваш возраст не подтвержден»

Необходимо реализовать валидацию на странице «Авторизации», которая была добавлена в процессе выполнения 10-го задания. Для реализации использовать подход FormBuilder.

  1. Поле «логин» должно быть не пустым его длина не может быть меньше 5 символов и не больше чем 25 символов. Поле логин должно соответствовать маске e-mail. Если это правило не выполняется, пользователь увидит соответствующее сообщение. Таким образом всего может быть 3 разных валидационных сообщения: «поле не может быть пустым», «не соответствует длинна», «неверный формат».

  2. Поле «пароль» не должно быть пустым, его длина не может быть меньше 5 символов и не больше чем 25 символов. В пароль обязательно должна входить хотя бы одна цифра и одна большая буква. Таким образом всего может быть три разных валидационных сообщения: «поле не может быть пустым», «не соответствует длина», «неверный формат».

HTTP

Что такое AJAX. Работа с AJAX в Angular, обзор и подключение HttpClientModule

AJAX (аббревиатура от «Asynchronous Javascript And Xml») – это подход к построению веб-приложений, где обмен данными между клиентом (веб-браузером) и сервером происходит в «фоновом» режиме без перезагрузки всей страницы.
Все SPA, в том числе разработанные на Angular, используют технологию AJAX для получения или отправки данных на сервер. Это дает возможность делать приложения с отзывчивым интерфейсом, похожим на десктопные приложения.
Для работы с AJAX в браузерах существует встроенный API на основе объекта XMLHttpRequest, который содержит все необходимые методы и более современный метод fetch, который имеет promise-like синтаксис. А также сторонние библиотеки, как axios.

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

import {HttpClientModule} from '@angular/common/http';
...
@NgModule({
  imports: [
    ...
    HttpClientModule
    ...
  ],
  ...
})
export class AppModule {
}

После подключения модуля HttpClientModule станет доступным для внедрения сервис HttpClient, который непосредственно и содержит целый ряд методов, для создания всех необходимых REST-запросов.
Имеет Observable-like API. Все методы для создания HTTP-запросов возвращают инстанс Observable, что открывает нам все возможности реактивного программирования.

import {HttpClient} from '@angular/common/http';
...
@Injectable()
export class UserService {
  
  constructor(private http: HttpClient, ...) {
  }
  ...
}

Основные CRUD-операции с помощью сервиса HttpClient.

Для создания GET-запроса в сервисе HttpClient доступен одноименный метод: get(‘’).
Метод принимает в качестве первого аргумента URL-адрес запроса, а также необязательный аргумент с заголовками и параметрами запроса, который рассмотрим позже.
Возвращает экземпляр класса Observable, который инициализирует GET-запрос в момент подписки на него методом subscribe.
В «поток» будет передан объект, созданный на основе «сырых» сериализированных json-данных, которые вернул сервер.

// в сервисе
import {HttpClient} from '@angular/common/http';
...

@Injectable()
export class UserService {

  private usersUrl = 'https://reqres.in/api/users';

  constructor(private http: HttpClient) {
  }

  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.usersUrl}/${id}`);
  }
}
// в компоненте
import ...

@Component({...})
export class UserEditComponent {
  user: User;
  constructor(
    private userService: UserService,
  ) {}
  ngOnInit() {
    this.userService.getUser(id)
      .subscribe((user: User) => {
        this.user = user;
      });
  }
}

Для создания POST-запроса в сервисе HttpClient доступен одноименный метод: post(‘<URL>’, <object>).
Принимает в качестве первого аргумента URL-адрес, на который будет осуществлен POST-запрос, а в качестве второго объект, который будет сериализирован и отправлен как тело POST-запроса.
Возвращает экземпляр Observable.
Инициализирует POST-запрос подписка методом subscribe на возвращаемый Observable.
Как правило, все REST-API серверы следуют такому «контракту», что на POST-запрос возвращают код состояния 201 Created и сохраненный объект в теле ответа, что бы была возможность отследить на «клиенте», если добавились дополнительные поля в объект после сохранения в БД, например id.

// в сервисе
import {HttpClient} from '@angular/common/http';
...

@Injectable()
export class UserService {

  private usersUrl = 'https://reqres.in/api/users';

  constructor(private http: HttpClient) {
  }

  createUser(user: User) {
    return this.http.post(this.usersUrl, user);
  }
  ...
}
// в компоненте
import {UserService} from ‘...';
...

@Component({...})
export class UserCreateComponent {
  user = {name: 'Alex', username: 'alex21’};

  constructor(
    private service: UserService,
  ) {}

  createUser() {
    this.service.createUser(this.user)
      .subscribe((user) => console.log(user));
  };
}

Методы PUT и DELETE также являются очень часто используемыми CRUD-операциями.
Для их создания HttpClient существуют одноименные методы:
put(‘<URL>’, <object>) и delete(‘<URL>’).
Метод put() принимает вторым аргументом объект, на основе которого будет обновлен уже существующий в БД. Своим программным интерфейсом в целом аналогичен методу post().
Метод delete() программным интерфейсом похожий на метод get(), как позитивный ответ от сервера может получить статус ‘OK’, а также сам удаленный объект, в зависимости от «контракта» REST-API сервера.

Обработка ошибок HTTP-запросов.

Как нам уже известно, все HTTP-методы сервиса HttpClient возвращают экземпляр Observable, поэтому обработка ошибок HTTP-запросов будет строится на программном интерфейсе объектов и методов библиотеки RxJS.
Для того, что бы отловить ошибку HTTP-запроса, у нас есть pipeble-оператор catchError и статический метод throwError.
Оператор catchError позволяет перехватить системную ошибку, метод throwError передает ошибку в «поток» тем, кто подписан на него методом subscribe.
Перехваченная ошибка в колбэке catchError есть экземпляром класса HttpErrorResponse, который содержит все необходимые поля с отладочной информацией: error, headers, status, url …
Обработка ошибки в «подписчике», как мы уже знаем, происходит во второй функции обратного вызова аргументов метода subscribe.

// метод в сервисе
getUser(id: number) {
  return this.http.get(`${this.usersUrl}/${id}`)
    .pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 404) {
          return throwError('Not found');
        } else {
          return throwError(err.message);
        }
      })
    );
}

// подписка в компоненте
this.service.getUser(id)
  .subscribe(
    user => {
      console.log(user);
    },
    error => {
      console.error(error);
    }
  );

Установка заголовков (HttpHeaders) и query-параметров в HTTP-запросах.

Для установки заголовков в HTTP-запросы в Angular существует специальная функция-конструктор HttpHeaders. Что бы создать заголовки необходимо вызвать ее с ключевым словом new. В функцию-конструктор можно передать необходимые заголовки в JSON-подобном формате:

headers: new HttpHeaders({
  'Content-Type':  'application/json',
  'Authorization': 'my-auth-token'
})

Что бы добавить новые ключ-значения в уже сконфигурированные HttpHeaders, можно воспользоваться методом set, который возвращает новый объект HttpHeaders:

let headers = new HttpHeaders({
  'Content-Type': 'application/json'
});
headers = headers.set('Authorization', 'my-auth-token');

Для установки query-параметров в HTTP-запросы в Angular существует специальная функция-конструктор HttpParams, которая вызывается с помощью оператора new и принимает ключ-значения в методе set как аргументы:

const params = new HttpParams().set('per_page', '9');

Передача в HTTP-запрос заголовков и параметров осуществляется в самом методе, как последний аргумент метода в виде объекта следующего вида:

getUsers(): Observable<User[]> {
    let headers = new HttpHeaders({
      'Content-Type': 'application/json'
    });
    const params = new HttpParams().set('per_page', '9');
    return this.http.get(this.usersUrl, {headers: headers, params: params});
}

Загрузка файлов.

Обычно отправку данных с клиента и сервер посредством Ajax-запросов удобно делать в json-формате, который преобразовывает пересылаемые данные в текстовый формат. Но такой подход не подходит для передачи в Ajax-запросе файлов, т.к. файлы содержать бинарные, а не текстовые данные.
Для решения подобной проблемы существует встроенная функция-конструктор FormData(), которая позволяет нам сделать отправку в Ajax-запросе обыкновенной формы с encoding установленным в “multipart/form-data”.
Такой формат позволяет передавать как и текстовые поля формы, так и файлы.
Работа с функцией-конструктором FormData в Angular ничем не отличается от работы с ней в JavaScript.
Сам экземпляр FormData передается вторым аргументом метода: post(<url>, <form-data>).
Также стоит отметить, что существует множество готовых директив для Angular 2+, которые дают упрощенный API для отправки файлов посредством Ajax-запросов: ng2-file-upload, angular-base64-upload, ngx-file-drop…

// метод сервиса
uploadFile(file: File): Observable<File> {
  const formData: FormData = new FormData();
  formData.append('file', file, file.name);
  const options = {
    headers: new HttpHeaders().set('Content-Type', 'multipart/form-data'),
  };
  return this.http.post<File>(`${this.usersUrl}/file`, formData, options);
}

Создание и подключение HttpInterceptor.

HttpInterceptor в Angular выполняют функции на подобие функций промежуточного слоя (middleware), которые очень популярны в современных backend-фреймворках (Express.js).
Их основная задача осуществлять перехват и изменение HTTP-запросов и ответов приложения. К таким задачам можно отнести установку заголовков, параметров, логирование, проверку на подлинность определенных параметров в ответах…
HttpInterceptor представляет собой сервис, который имплементирует интерфейс HttpInterceptor, то есть должен содержать метод intercept, в котором непосредственно и происходит изменение HTTP-запроса.

import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';

@Injectable()
export class AuthInterceptor
  implements HttpInterceptor {
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('auth_token');
    const newReq = req.clone({
      setHeaders: {
        'Authorization': `Bearer ${token}`
      }
    });
    return next.handle(newReq);
  }
}

Подключение HttpInterceptor в приложение происходит так же, как и с сервисами в модуле в секции с помощью встроенного токена HTTP_INTERCEPTORS.
Так как с помощью встроенного токена HTTP_INTERCEPTORS могут инжектироваться разные «кастомные» HttpInterceptor-ы, необходимо указывать параметр multi: true, который и позволяет следующее поведение инжектора.

import {HTTP_INTERCEPTORS} from '@angular/common/http’;
import {AuthInterceptor} from ‘...’;
import {OtherInterceptor} from ‘...';
...
providers: [
  ...
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: OtherInterceptor, multi: true }
],

// dep = injector.get(HTTP_INTERCEPTORS)
// dep == [new AuthInterceptor(), new OtherInterceptor()]

Стоит также обратить внимание на порядок выполнения HttpInterceptor-ов.
Если мы подключаем несколько HttpInterceptor-ов, то запрос (HttpRequest) будет проходить через них в том же порядке подключения в массиве providers. А если ми обрабатываем ответ (HttpResponse), то он будет проходить по HttpInterceptor−ам в обратном порядке.

Несколько полезных RxJS-техник при работе с HTTP

debounceTime(). Оператор debounceTime(<time, ms>) принимает в качестве аргумента время задержки в миллисекундах. Позволяет задержать выполнение подписки на это время. Очень популярный метод при работе с HTTP, если необходимо делать HTTP-запрос на основе введенных пользователем данных, что бы избежать большого количества «холостых» запросов.

const input = document.getElementsByTagName('input');
fromEvent(input, 'keyup')
  .pipe(
    pluck('target', 'value'),
    debounceTime(2000)
  )
  .subscribe((query: string) => this.searchService.makeSearch(query));

distinctUntilChanged(). Оператор distinctUntilChanged() «заэмитит» значение в «поток» только в том случае, если следующее значение отличается от предыдущего.
Удобный метод при работе с HTTP, если запросы зависят то пользовательского ввода на событие (keyup), а пользователь вместо ввода текста делает навигацию стрелками в поле ввода, соответственно значение в поле ввода не изменяется при этом.

const input = document.getElementsByTagName('input');
fromEvent(input, 'keyup')
  .pipe(
    pluck('target', 'value'),
    debounceTime(2000),
    distinctUntilChanged()
  )
  .subscribe((query: string) => this.searchService.makeSearch(query));

retry(). Оператор retry(n) принимает в качестве аргумента количество повторных попыток «заэмитить» значение в «поток» в случае возникновения ошибки.
Удобный оператор при работе с HTTP, если возникает необходимость сделать повторный HTTP-запрос, в случае неудачного соединения или ошибки сервера.

deleteUser(id: number): Observable<any> {
  return this.http.delete(`${this.usersUrl}/${id}`)
    .pipe(
      retry(2)
    );
}

Очень часто возникает задача, когда нам необходимо получить данные из бэкенда, и на основе полученных данных сделать еще один HTTP запрос и сохранить конечный результат. Что бы избежать вложенных subscribe() нам может пригодится методы mergeMap / flatMap.

@Component({…})
export class AppComponent {
  user$: Observable<{}>;
  constructor(private http: HttpClient) { }

  ngOnInit() {
    this.user$ = this.http.get('/api/people/1').pipe(
      mergeMap(user => this.http.get(user.portfolio))
    );
    this.user$.subscribe(portfolio => console.log(portfolio))
  }
}

Proxy-config.

Часто при разработке приложения возникает задача получать данные из сервера. который запускается на отличном от localhost домене или на другом порту даже на localhost. Обычно, такие серверы не настроены по умолчанию на получение кросс-доменных запросов с нашего локального проекта, запущенного на домене localhost:4200 , например.
Что бы избавится проблемы CORS-блокировки, у Angular-CLI проектов для этого есть встроенные средства, а точнее необходимо настроить proxy-config, что бы все запроси «проксировались» к бэкенду с помощью webpack-dev-server-а.
Для этого необходимо создать конфигурацию в виде обычного node.js-файла:

// proxy.conf.js
const PROXY_CONFIG = [
  {
    context: [‘/api’, ...],           // какие эндпоинты проксируем
    target: 'http://localhost:3000’,  // куда проксируем
    secure: false                     // что бы отключить CORS
  }
]

module.exports = PROXY_CONFIG;

Запуск сборки проекта теперь будет выглядеть: > ng serve —proxy-conf=proxy.conf.js

Задача.

Решением данной задачи, будет продолжение задачи из сессии №10 Routes.
Вам необходимо перенести статические данные, которые использовались в предыдущей задаче на сервис mockapi.io.
После того, как будет готовый REST-api на mockapi.io, реализовать в приложении из сессии №10 возможность покупки телефона, возможность добавить новый телефон, удалить телефон и редактировать поля уже существующего, вызывая необходимые HTTP-методы с запросами к mockapi.io.

ДОМАШНЕЕ ЗАДАНИЕ

В этом задание продолжим работу над приложением о фильмах.
В предыдущем задание мы делали авторизацию на “mock”-данных. Пришло время сделать нашу авторизацию “настоящей” и для этого мы будем использовать уже знакомое нам API themoviedb.

  1. Сделать авторизацию в приложении, используя для этого API themoviedb. Механизм взаимодействия с API themoviedb детально описан в дополнительной инструкции по ссылке. Обратите внимание, что некоторые запросы к API необходимо отправлять последовательно, после получения некоторых данных с предыдущих запросов. Постарайтесь для этого использовать лучшие подходы библиотеки RxJS.

  2. Получать из данных нашего пользователя, зарегистрированного на themoviedb список “Избранное” и отображать соответствующим образом эти фильмы в нашем приложении специальной иконкой или другим образом, как это было и до этого момента. Переделать функционал добавления в “Избранное” в нашем приложении таким образом, что бы добавленный фильм попадал в список “Избранное” нашего пользователя на themoviedb и соответственно удалялся из него.

CUSTOM DIRECTIVES

Директивы — мощный инструмент в «арсенале» Angular, который дает возможность изменять свойства и поведение DOM-элементов, реагировать на пользовательские события и изменять структуру DOM-дерева.
Директивы уже были рассмотрены ранее. Вспомним некоторые ключевые моменты.

Существует три вида директив:

  • — Компоненты – директивы, у которых есть собственный шаблон
  • — Атрибутивные директивы – изменяют внешний вид или поведение DOM-элемента, компонента или другой директивы
  • — Структурные директивы – могут вносить изменения в DOM-дерево, добавляя или удаляя в нем элементы

Кастомные атрибутивные директивы

Атрибутивные директивы – это директивы, которые изменяют внешний вид или поведение DOM-элементов.

Как мы уже знаем, Angular имеет много встроенных атрибутивных директив, к которым относятся:

  • — Директивы модуля Angular: NgStyle , NgClass
  • — Директивы модуля форм Angular: NgForm , ngModel , ngModelGroup
  • — Директивы модуля маршрутизации: RouterLink , RouterOutlet

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

  • — Создать класс, аннотированный декоратором @Directive
  • — Декоратор @Directive принимает объект с ключом selector (есть другие, необязательные ключи inputs, outputs, providers, …)
  • — Значение ключа selector должно соответствовать стандартным CSS-селекторам атрибутов
  • — Инжектировать в конструкторе директивы ElementRef, экземпляр которого представляет собой ссылку на DOM-элемент в виде единственного поля nativeElement
  • — Инжектировать Renderer2 объект, с помощью которого происходит все взаимодействие с DOM-элементами
  • — Подключить директиву в модуль, в секции declarations (так же, где подключаются компоненты)

Имея ссылку на DOM-элемент через ElementRef.nativeElement легко обращаться к его свойствам и устанавливать значения, например:

ngOnInit() {
  this.el.nativeElement.style.color = 'blue';
} 

На первый взгляд такой подход может показаться вполне логичным, но он имеет некоторые недостатки.
Поскольку идет прямое обращение к свойствам DOM-элементов, получается жесткая привязка нашего приложения к DOM, то есть непосредственно к браузеру.
Фреймворк Angular имеет все возможности для написания не только браузерных SPA-приложений, а и нативных мобильных (NativeScript), web-view мобильных (Ionic), web-view десктопных (Electron) приложений. По этой причине рекомендуется не обращаться к свойствам элементов напрямую, а использовать специальную абстракцию Renderer, которая в зависимости от типа приложения будет применять к DOM- или нативным элементам те или иные свойства.
Поскольку классу Renderer необходимо осуществлять поддержку всевозможных платформ для Angular приложений, его API существенно изменился после выхода Angular версии 2 . В текущей версии 6 рекомендуется использовать класс Renderer2. Так же доступный для импорта класс Renderer3, но его использовать не рекомендуется из-за нестабильности нового API. Устарелый класс Renderer остался доступным для поддержки обратной совместимости директив между разными версиями Angular.
Класс Renderer2 используется в основном для манипуляций над уже существующими элементами, например для изменения стилей элемента, атрибутов и параметров элемента. Он также позволяет создавать новые элементы и вставлять их в DOM.
Предыдущий пример теперь будет выглядеть:

constructor(
  private el: ElementRef,
  private renderer: Renderer2
) {}
ngOnInit() {
  // this.el.nativeElement.style.color = 'blue';
  this.renderer.setStyle(this.el.nativeElement, 'color', 'blue');
}

Пример кастомной атрибутивной директивы полностью:

import {Directive, ElementRef, OnInit, Renderer2} from '@angular/core';

@Directive({
  selector: '[appPrimaryColor]'
})
export class PrimaryColorDirective implements OnInit {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit() {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', '#0094d2');
    this.renderer.setStyle(this.el.nativeElement, 'color', '#fafafa');
  }
}

Декоратор @HostListener позволяет подписываться на события DOM-элементов, к которым применяется директива и «привязать» к этим событиям необходимые методы директивы.
Декоратор применяется к методам, которые станут функциями-обработчиками на событие элемента. Он принимает строку с названием события, таким образом произойдет привязка к событию.
Вторым аргументом можно передать массив строк. Данный массив строк будет содержать свойства, которые будут переданы в обработчик события. С помощью такого синтаксиса легко передать объект события, например $event.

@HostListener('mouseenter', ['$event']) onMouseEnter(event) {

  // получаем событие event как первый аргумент функции обработчика
  console.log(event); // работа с событием

  // работа со свойствами хост-элемента
  this.renderer.setStyle(this.el.nativeElement, 'color', '#fafafa');
}

Декоратор @HostListener имеет ряд преимуществ:

  • — Не «обращаемся» к DOM напрямую
  • — Подписка на события уничтожается автоматически после завершении жизненного цикла директивы, что дает возможность избежать «утечек» памяти
  • — Имеет более простой API по сравнению с методом EventTarget.addEventListener()

С помощью декоратора @HostBinding можно легко связать свойства DOM-элементов, к которым применяется директива с полем класса директивы.
Декоратор @HostBinding применяется к полю класса директивы и принимает в виде строки название свойства DOM-элемента, если названия свойства и поля класса совпадают то параметр можно не указывать.
Таким образом у нас получается однонаправленная привязка. Свойство DOM-элемента после применения директивы получит «привязанное» значение поля директивы.
Изменяя «привязанное» значение поля директивы внутри каких-то методов динамически, будет изменятся соответствующее DOM-свойство хост-элемента.
Можно также сказать, что данный способ взаимодействия со свойствами элементов альтернативный способу задания свойств через Renderer2.

  1. @HostBinding(‘style.cursor’) cursor;

  2. @HostBinding() innerHtml;

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

import {Directive, ElementRef, HostBinding, HostListener, Input, OnInit, Renderer2} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {

  @Input('appHighlight') highlightColor: string;

  // @HostBinding через свойство класса
  @HostBinding('style.cursor') cursor;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  // @HostBinding через геттер
  @HostBinding('style.fontVariant') get fontVariant() {
    return 'small-caps';
  }

  ngOnInit() {
    this.cursor = 'pointer';
  }

  @HostListener('mouseenter', ['$event']) onMouseEnter(event) {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', this.highlightColor);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.renderer.setStyle(this.el.nativeElement, 'background-color’, ‘transparent’);
  }
}

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

  1. @Input() defaultColor: string;

Передача свойств в директиву происходить также, как и с обычной привязкой свойств к DOM-элементам:

  1. <div
  2. appHighlight
  3. [defaultColor]="‘violet’"
  4. >

Следует напомнить о существовании механизма «алиаса». В шаблоне и внутри декоратора можно указать одно название параметра (appHighlight), а в коде класса директивы использовать другое имя (hilightColor):

...
@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
  ...
  @Input('appHighlight') highlightColor: string;
  ...
}

В таком случае передача свойства будет выглядеть следующим образом:

  1. <div
  2. [appHighlight]="‘violet’"
  3. >

Кастомные структурные директивы

Структурные директивы – это директивы, которые могут изменять DOM-структуру элементов, к которым они применяются.
Напомним что оператор asterisk (*) автоматически обернёт элемент, к которому применяется структурная директива в шаблон ng-template. В DOM такая структура будет выглядеть как текстовый узел комментария со специфическим синтаксисом.

<ng-template [myIf]="condition">
  <p>Angular is the best framework.</p>
</ng-template>

Однако в реальной работе пользуются привычной записью, которая указани ниже.

<p *myIf="condition">Angular is the best framework.</p>

Angular имеет несколько встроенных структурных директив, синтаксис которых легко распознать по знаку * в имени директивы.
К встроенным структурным директивам можно отнести: NgIf , NgFor и NgSwitch .
Напомним как выглядит их, уже знакомый нам синтаксис:

<div *ngIf="hero" class="name">{{hero.name}}</div>

<ul>
  <li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>

<div [ngSwitch]="hero?.emotion">
  <app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
  <app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
  <app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>

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

  • — Создать класс, аннотированный декоратором @Directive
  • — Декоратор @Directive в качестве параметра принимает объект з полем selector, …
  • — Инжектировать в конструкторе класс TemplateRef, который содержит ссылку на шаблон
  • — Инжектировать в конструкторе класс ViewContainerRef, который представляет собой контейнер (ng-container), куда будет отображаться шаблон и содержит методы для отображения
  • — Инжектировать в конструкторе класс Renderer2, если есть необходимость изменять свойства элементов или создавать новые узлы
  • — Подключить директиву в модуль, в секции declarations

Что бы отобразить шаблон (директивы) в хост-элемент, необходимо у объекта класса ViewContainerRef воспользоваться методом createEmbeddedView, который принимает как аргумент экземпляр шаблона хост-элемента TemplateRef.

@Directive({
  selector: '[appDumb]'
})
export class DumbStructuralDirective {

  constructor(
    private templateRef: TemplateRef<void>,
    private viewContainerRef: ViewContainerRef,
  ) {}
  
  ngOnInit() {
    this.viewContainerRef.createEmbeddedView(this.templateRef);
  }
}

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

Используя метод resolveComponentFactory класса ComponentFactoryResolver, который принимает динамический компонент как аргумент, определяем «фабрику» для создания нового преставления. Полученная «фабрика» передается в метод createComponent класса ViewContainerRef для отображения ее на хост-элементе.

Необходимо помнить, что динамические компоненты должны быть объявленными в поле entryComponents декоратора модуля NgModule. Это не даст Webpack возможности удалить компонент из сборки как неиспользуемый.

Если возникает необходимость очистить представление перед отображением компонента или шаблона, существует метод clear у класса ViewContainerRef.

@Directive({
  selector: '[appDumb]'
})
export class DumbStructuralDirective {
  constructor(
    private templateRef: TemplateRef<void>,
    private viewContainerRef: ViewContainerRef,
    private cfr: ComponentFactoryResolver
  ) {}

  ngOnInit() {
    this.viewContainerRef.clear();
    const cmpFactory = this.cfr.resolveComponentFactory(DynamicComponent);
    this.viewContainerRef.createComponent(cmpFactory);
  }
}

В этом задании необходимо будет попрактиковаться в создании различных “кастомных” директив.
Для выполнения задания воспользуйтесь заготовкой на Stackblitz. Для работы с заготовкой необходимо нажать кнопку “FORK” в верхней части меню.

  • — Создать директиву, которая все внешние ссылки (те, у которых есть атрибут href ) подсветит розовым цветом. Обратите внимание на атрибутивные CSS-селекторы, которые используются в директивах.
  • — Создать директиву, которая всем заголовкам h4 установит в CSS-свойство font-weight значение bold. Используйте для этой задачи методы класса Renderer2.
  • — Создать директиву, которая будет следить за событием наведения мышки на карточку <mat-card> в компоненте MainComponent. При наведении мышки на карточку подсвечивать ее определенным цветом. Значение цвета должно передаваться в директиву в виде текста, используя @Input-свойство. Решите данную задачу посредством декораторов @HostListener и @HostBinding.
  • — Создать директиву, которая применяется к картинкам в шаблонах компонент и реализует следующую логику: пока будет происходить загрузка оригинальной картинки, она будет подменена картинкой-заглушкой, в которой будет отображаться лоадер или спиннер (картинку для спиннера можно использовать по след. ссылке), а когда оригинальная картинка загрузится, она будет отображена на своем месте.
    Для реализации этой директивы можно придерживаться такой последовательности:
    • — Внедрить в директиве ElementRef и Renderer2
    • — Произвести всю нижеперечисленную последовательность действий внутри метода ngAfterViewInit жизненного цикла
    • — Получить ссылку на оригинальную картинку методом getAttribute(), которая находится в атрибуте src, и сохранить ее в переменную
    • — Установить вместо оригинальной картинки ссылку на картинку-заглушку в атрибуте srcметодом setAttribute объекта Renderer2
    • — Создать новый элемент картинки методом createElement объекта Renderer2
    • — Методом setAttribute установить новому элементу картинки ссылку в атрибут srcот оригинальной картинки
    • — Методом listen объекта Renderer2 подписаться на событие load нового элемента картинки
    • — В функции обратного вызова подписки на load методом parentNode объекта Renderer2 получить ссылку на родителя элемента картинки
    • — Методом insertBefore объекта Renderer2 вставить новый элемент картинки
    • — Методом removeChild объекта Renderer2 удалить элемент картинки со спиннером

PIPES

Пайпы — специальные конструкции, которые предназначены для преобразования данных «налету» прямо в шаблоне.
Существует определенное количество системных пайпов.
Так же есть возможность создать свои собственные «кастомные» пайпы.

import { Component } from '@angular/core';

@Component({
  selector: 'user-details',
  template: `<p>Имя пользователя: {{name | uppercase}}, 
                День рождения пользователя: {{ birthday | date }}</p>`
})
export class UserComponent {
  public birthday = new Date(1985, 3, 15);
  public name = 'john connor';
}

Для использования пайпа в шаблоне необходимо указать его название после символа «|».

Таким образом данные будут отображены не в исходном, а в уже преобразованном виде.

Angular поставляется с набором встроенных пайпов.

  • — AsyncPipe
  • — DatePipe
  • — KeyValuePipe
  • — JsonPipe
  • — CurrencyPipe
  • — DecimalPipe
  • — PercentPipe
  • — SlicePipe
  • — UpperCasePipe
  • — TitleCasePipe
  • — LowerCasePipe

Описывать каждый из них нет никакой необходимости.
К каждому из них есть понятная документация с примерами, доступная по ссылке: https://angular.io/api?type=pipe
Многие пайпы поддерживают дополнительную конфигурацию, которая указывается через двоеточие.

import { Component } from '@angular/core';

@Component({
  selector: 'user-details',
  template: `<p>День рождения пользователя: {{ birthday | date:"MM/dd/yy"}}</p>`
})
export class UserBirthdayComponent {
  public birthday = new Date(1985, 3, 15);
}

Рассмотрим несколько примеров работы со встроенным пайпами.

  • — TitleCasePipe
  • — UperCasePipe
  • — DatePipe
  • — SlicePipe
import { Component } from '@angular/core';

@Component({
  selector: 'user-details',
  template: `<p>Полное Имя пользователя: {{name | titlecase}}, 
                Имя {{name | slice:0:3  | uppercase}},
                Фамилия {{name | slice:5:10 | uppercase}},
                День рождения пользователя: {{ birthday | date:"MM/dd/yy"}}</p>`
})
export class UserBirthdayComponent {
  public birthday = new Date(1985, 3, 15);
  public name = 'john connor';
}

CUSTOM PIPES

Существует возможность написать свой собственный pipe. Однако в этом есть смысл только если вы часто производите одинаковые преобразования в шаблоне.

  • — Объявить класс с декоратором @Pipe()
  • — Передать в качестве параметра объект, одним из полей которого будет название нового pipe
  • — Реализовать метод transform. Метод принимает как минимум 1 параметр. Первый – это величина, к которой применяется pipe. Второй и последующие – параметры которые принимает pipe.
import { Pipe, PipeTransform } from '@angular/core';
/*
 *   {{ 'это длинный текст который однозначно надо укоротить' | textReducer:10 }}
*/
@Pipe({name: 'textReducer'})
export class TextReducerPipe implements PipeTransform {
  transform(value: string, size: number): string {
    return value.substring(0, size) + '...';
  }
}

PIPES AND CHANGE DETECTION

Существуют так называемые «чистые» и «не чистые» пайпы (pure and impure pipes).
Разница заключается в механизме определения изменений того элемента к которому применяется pipe.
Pure pipes. Выполняются только в том случае если происходит изменение примитива (String, Number, Boolean, Symbol) или меняется ссылка на объект

(Date, Array, Function, Object).

Большинство встроенных pipes. Наибольшая производительность.

Impure pipes. Выполняются при каждом цикле определения изменений (change detection cycle). То есть фактически при каждом клике мышкой и при нажатии каждой кнопки
Использовать при крайней необходимости. Может работать медленно.
Для реализации custom impure pipe необходимо прописать дополнительный параметр в декораторе @Pipe():

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'dataFilter',
     pure: false
})
export class DataFilterPipe implements PipeTransform {
  transform(value: array, filter: string) {
    return value.filter(item => item.status === filter);
  }
}
<div class="row" *ngFor="let post of posts | dataFilter:'published'">
    <div>{{post.title}}</div>
    <div class="card-text">{{post.body}}</div>
</div>

В случае pure: false, если в массив posts будет добавлен новый элемент, pipe определить изменения, запуститься и «отфильтрует» значения.

Если прописать pure: true или забрать этот ключ совсем при добавлении нового элемента в массив posts ничего не произойдет.

Impure pipes рекомендуется использовать только в самых крайних случаях, поскольку они сильно влияют на производительность.
Если вам необходимо реализовать фильтрацию или сортировку, команда Angular рекомендует делать это в коде самого компонента.

Одним из наиболее популярных Impure pipes является AsyncPipe.

ASYNCPIPE

Подписывается на изменения Observable или Promise и отображает данные которое они возвращают. Позволяет не отображать данные в шаблоне пока они фактически не будут получены.

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css']
})
export class PostsComponent implements OnInit {
  private posts$: Observable<Post[]>;
  constructor(private service: PostsService) {}

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

  ngOnInit() {
    this.service.getPosts().subscribe((data: Post[]) => {
        this.posts$ = of(data);
    });
  }
}
<div class="row" *ngFor="let post of posts | async ">
    <div>{{post.title}}</div>
    <div class="card-text">{{post.body}}</div>
</div>

ДОМАШНЕЕ ЗАДАНИЕ

Это финальное задание для нашего приложения о фильмах. Цель этого задания привести в порядок приложение, над которым мы работали. Поработать с внешним видом самого приложения, «отрефакторить» код приложения и добавить немного дополнительного функционала.

      1. В предыдущем задании была реализована функция входа в систему на основе данных из онлайн-сервиса themovie.db. Как мы уже видели в документации к API, у нас есть возможность получить данные пользователя. Необходимо добавить отображение имени пользователя (или логина) в верхней правой части тулбара. Также добавить возможность по нажатию на соответствующую кнопку, выходить из системы (разлогиниваться, удалить сессию), используя для этого запрос к API — Delete Session .
      1. Дооформить страницу с детальным отображением информации о фильме. Обязательно добавить отображение информации о рейтинге, текстовое описание (обзор). Кнопки для добавления в “избранное” и “закладки”, должны присутствовать на странице детального отображения информации и функционировать. Как мы знаем в информации о фильме с API приходят данные со списком актеров, которые связанны с этим фильмом. Используя соответствующий компонент для отображения актера в списке(например FilmListComponent, который вы используете для построения списка на заглавной странице для фильмов), отобразить связанных актеров на странице детальной информации о фильме. Также у нас есть возможность получить список видеороликов (трейлеров), которые относятся к определенному фильму. Постараться отобразить их, используя YouTube Player API в виде IFrame или, для простоты, хотя бы ссылками на них. Разбить секции “детальная информация”, “связанные актеры” и “видеоролики” на отдельные табы. По желанию, сделать возможность сохранения в localStorage информацию о том, какой из табов был открыт для определенного фильма последний раз и соответственно открывать его при повторном переходе на страницу детальной информации.
    1. Так же дооформить страницу с детальной информацией об актёре. Должна отображаться информация об актере, биография и связанные фильмы с постерами. По желанию реализовать в виде таблички или специального виджета timeline (есть готовые решения, например angular-mgl-timeline), а в нем в хронологическом порядке отобразить фильмы, в которых принимал участие данный актер.

МОДУЛЬНОЕ ТЕСТИРОВАНИЕ В ANGULAR

Понятие модульного тестирования

Тестирование – обширная тема, которую невозможно полностью рассмотреть в пределах одного урока. Однако мы рассмотрим самые ключевые моменты.
В целом тестирование разделяется на 3 группы:

  • — Модульное (Unit) тестирование;
  • — Интеграционное тестирование;
  • — End-to-end тестирование.

Тестирование приложений на Angular, в основном подразумевает написание модульных (unit) тестов.

  • — Модульные тесты дают четкое понимание того, что отдельные части приложения работают именно так, как мы этого ожидаем;
  • — Дают возможность проводить тестирование в различных тестовых окружениях, используя разные наборы mock-данных;
  • — Дают максимальное покрытие тестами разных сущностей фреймворка;
  • — Относительно простые в написание;

Инструменты для модульного тестирования в Angular

Для выполнения модульного тестирования в проектах, созданных с помощью Angular-cli, по умолчанию устанавливаются:

  • — Jasmine — популярный фреймворк модульного тестирования;
  • — Karma — система исполнения тестов, которая отслеживает файлы проекта и запускает тесты, определенные с использованием Jasmine, при обнаружении изменений.

В папке фреймворка “@angular/core/testing” находится набор дополнительных классов и функций, которые необходимы для более простого написания модульных тестов, с учетом специфичности самого фреймворка Angular.

Jasmine

Фреймворк Jasmine имеет удобный API для написания модульных тестов.
Рассмотрим несколько важных методов:

Метод Описание
describe(description, function) Метод используется для
группировки взаимосвязанных тестов
beforeEach(function) Метод используется для выполнения задач,

которые должны
выполняться перед каждым модульным тестом|
|afterEach(function)|Метод используется для выполнения задач,
которые должны
выполняться после каждого модульного теста|
|it(description, function)|Метод используется для
выполнения тестового действия|
|expect(value)|Метод используется для
получения результата теста|
|toBe(value)|Метод задает ожидаемое значение теста|

Посмотрим как выглядит самый простой тест на Jasmine:

describe('our test runner', () => {
  it('is alive!', () => {
    expect(true).toBe(true);
  });
});

Что касается unit-тестирования Angular приложений с использованием фреймворка Jasmine, то можно выделить два вида тестов:

  • — Изолированные – это тесты, которые не зависят от самого Angular. К таким тестам часто относятся тесты для пайпов и сервисов, т.к. данные сущности имеют мало зависимостей от фреймворка.
  • — Тесты с созданием окружения, в которых с помощью тестовой утилиты TestBedосуществляется настройка и инициализация среды для тестирования. Класс содержит методы, которые облегчают процесс тестирования. К такому типу тестов можно отнести тесты для директив и компонент.

Тестирование пайпов

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

import {TextReducerPipe} from './text-reducer.pipe';

const inputValue = 'Lorem ipsum dolor sit amet';
const outputValue = 'Lorem ipsum dolor ...';
const size = 18;

describe('TextReducerPipe', () => {
  it('create an instance', () => {
    const pipe = new TextReducerPipe();
    expect(pipe).toBeTruthy();
  });

  it('should transform', () => {
    const pipe = new TextReducerPipe();
    expect(pipe.transform(inputValue, size)).toBe(outputValue);
  });
});

Класс TestBed

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

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

Рассмотрим несколько важных методов класса TestBed:

    • configureTestingModule – используется для создания и настройки модуля Angular, который создается на время тестирования. В метод можно передать объект с полями declarations и providers для указания подключаемых компонент, директив, и сервисов.
    • createComponent – метод для непосредственного создания компонента в тестовой среде.
    • compileComponents – для связывания свойств и методов между компонентом и его шаблоном, выполняется асинхронно.

Тестирование компонент

Для проведения unit-тестирования компонент необходимо создавать их экземпляры, для этого используется метод createComponent класса TestBed. Аргумент метода createComponent сообщает TestBed, экземпляр какого компонента следует создать. Результатом выполнения будет специальный объект – экземпляр класса ComponentFixture. Этот объект содержит необходимые методы и свойства для тестирования компонента.

Метод/свойство Описание
componentInstance возвращает объект (экземпляр) компонента
debugElement возвращает тестовый
элемент для компонента
detectChanges() выполняет change-detection в экземпляре компонента
whenStable() возвращает объект Promise, который «зарезолвится» при полной обработке всех изменений

Поскольку компонент, содержит шаблон, необходима некая стадия компиляции шаблона . При тестировании компонента шаг компиляции должен выполнятся явно, с помощью метода compileComponents класса TestBed .
Сам процесс компиляции происходит асинхронно, поэтому для упрощения синтаксиса в модуле @angular/core/testing добавлена служебная функция async , которая отлично комбинируется с методом beforeEach (см. пример) .
Имея доступ к экземпляру компонента через поле componentInstance объекта класса ComponentFixture , можем обращаться напрямую к методам и свойствам компонента.
Поле класса debugElement открывает большие возможности по тестированию отрендеренного шаблона тестируемого компонента. Через его поле nativeElement , например можем получить доступ к DOM-элементу компонента, а методом triggerEventHandler инициализировать необходимые события на DOM-элементах.

class MockDataSource {
  public data: DataModel[] = [
    {name: 'Test John', username: 'john-test', access: 'test'}
  ];

  getData(): Observable<DataModel[]> {
    return of(this.data)
      .pipe(delay(1000));
  }
}

describe('FirstComponent', () => {
  let component: FirstComponent;
  let fixture: ComponentFixture<FirstComponent>;
  const dataSource = new MockDataSource();

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [FirstComponent],
      providers: [
        {provide: FirstService, useValue: dataSource}
      ]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(FirstComponent) as ComponentFixture<FirstComponent>;
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('performs async operations', async(() => {
    dataSource.data.push({name: 'Test Alex', username: 'alex-test', access: 'test'});

    fixture.whenStable().then(() => {
      fixture.detectChanges();
      expect(component.data.length).toBe(2);
    });
  }));

  it('should create three li-elements', async(() => {
    dataSource.data.push({name: 'Test Peter', username: 'peter-test', access: 'test'});
    const compiled = fixture.debugElement.nativeElement;

    fixture.whenStable().then(() => {
      fixture.detectChanges();
      expect(compiled.querySelectorAll('li').length).toBe(3);
    });
  }));
});

Тестирование сервисов

Сервисы, в отличие от компонент, не зависят от внешней среды фреймворка и могут быть протестированные изолировано. Но разработчики Angular рекомендуют тестировать сервисы, подключая их в секции providers метода configureTestingModule класса TestBed .

import { TestBed, inject } from '@angular/core/testing';
import { DemoService } from './demo.service';

describe('DemoService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [DemoService]
    });
  });

  it('should be created', inject([DemoService], (service: DemoService) => {
    expect(service).toBeTruthy();
  }));

  it('should return data', inject([DemoService], (service: DemoService) => {
    expect(service.getDemoData()).toBe('Demo data');
  }));
});