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

ROOT MODULE AND COMPONENT BASICS

Корневой модуль (Root Module)

Модуль – структурная единица Angular приложения, которая объединяет вместе другие части приложения (компоненты, директивы, пайпы и сервиса) в одну связанную единицу (функциональную группу).

В модуль обычно включают части приложения, сгруппированные по смыслу. Например: LoginModule, UserInfoModule, SettingsModule.

В приложении должен быть хотя бы один модуль ( Root Module ). С него начинается процесс загрузки. Так же могут присутствовать так называемые « Feature modules «, которые включают определенные части приложения, сгруппированные логически.

Модуль объявляется с помощью декоратора @NgModule .

Основные property, которые входят в декоратор:
declarations – объявляет компоненты директивы и пайпы, которые принадлежат этому модулю;
exports – набор деклараций, которые должны быть видны в шаблоне компонента и в других модулях;
imports – другие модули, которые должны быть доступны (подключены) в текущем модуле;
providers – сервиса, которые данный модуль делает доступными во всем приложении;
bootstrap – прописывается один раз для главного модуля. Указывает на компонент с которого начинается загрузка приложения.

Пример главного модуля:

// подключение самого декоратора модуля
import { NgModule } from '@angular/core';
// один из самых главных модулей, указывает что мы работаем в браузере
import { BrowserModule  } from '@angular/platform-browser';
// модуль для работы с backend, позволяет выполнять http запросы
import { HttpModule } from '@angular/http';

// корневой компонент приложения
import { AppComponent } from './app.component';
// роутер модуль
import { AppRoutingModule } from './app-routing.module';
// пользовательский модуль
import { FilmCatalogModule } from './film-catalog/film-catalog.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FilmCatalogModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Пример роутер модуля:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MainComponent } from './film-catalog/main/main.component';
import { FilmsComponent } from './film-catalog/films/films.component';

const routes: Routes = [
  { path: "", pathMatch: "full", redirectTo: "main" }, 
  { path: "main", component: MainComponent },
  { path: "films", component: FilmsComponent}
  ];

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

Пример фича модуля:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MainComponent } from './main/main.component';
import { FilmsComponent } from './films/films.component';
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule
  ],
  declarations: [
    MainComponent, 
    FilmsComponent
  ]
})
export class FilmCatalogModule { }

Что такое компонент в Angular

Компонент – основная структурная единица Angular приложения, которая «контролирует определенную часть экрана».
Если упростить то, Angular приложение это дерево компонентов, вложенных друг в друга и объединенных в модули.

Основные особенности компонента:
— компонент является TypeScript классом, который содержит бизнес логику;
— каждый компонент тесно взаимодействует со своим шаблоном;
— поля класса компонента с помощью специального механизма, могут быть отображены сразу в шаблоне без прямого взаимодействия с DOM структурой;
— HTML код шаблона содержит специальный Angular синтакс;
— настройка компонента происходит с помощью Декоратора, специального синтаксиса TypeScript.

Простейший компонент:

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

@Component({
  selector: 'selector-name',
  templateUrl: 'name.component.html'
})

export class NameComponent {
  constructor() { }

  doSomething() { }
}
// импорты, для подключения необходимых пакетов
import { Component, OnInit } from '@angular/core'; 

// Декоратор, функциональность TypeScript. 
// Превращает обычный класс в Angular компонент
// Позволяет настроить компонент используя набор определенных настроек
@Component({
  selector: 'selector-name',
  templateUrl: 'name.component.html'
})

// Класс компонента.
// Ключевое слово exports позволяет в дальнейшем 
// импортировать (подключать) соответствующий класс в другом месте приложения
export class NameComponent {
  // поля класса, обявленные с указанием типа данных, которые они будут содержать
  value: string;
  films: object[];  

  // Конструктор (обычно используется для внедрения сервисов)
  constructor(public filmService: FilmService) { }


  // обычный метод
  doSomething() { }
  
  // хуки жизненного цикла или lifecycle hooks. 
  // Автоматически запускаются в определенный моменты выполнения приложения.
  ngOnInit() { }
}

Декоратор и его назначение

Декоратор – часть синтаксиса TypeScript.

По сути это функция, которая позволяет сконфигурировать компонент.

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

@Component({
  selector: 'selector-name',
  templateUrl: 'name.component.html'
})

Основные свойства декоратора компонента:
selector : string[] — css селектор с помощью которого компонент будет вставлен в шаблон;
styleUrls : string[] — список url к css файлам, которые будут применены к этому компоненту;
styles : string[] — инлайновый способ указания стилей, которые будут использоваться для компонента;
template : string[] — инлайновый способ указания шаблона;
templateUrl : string[] — url к файлу шаблона для этого компонента;
inputs : string[] — список проперти класса, которые принимают участие в связывании данных (data-bind) и принимают данные извне;
outputs : string[] — список проперти класса, которые принимают участие в связывании данных (data-bind) и отдают данные внешним компонентам;
providers : provider[] — список провайдеров (сервисов), которые будут использоваться в этом компоненте.

Дополнительные свойства декоратора компонента:
animation : any[] — настройка анимации компонента;
changeDetection : ChangeDetectionStrategy — стратегия отслеживания изменений;
encapsulation : ViewEncapsulation — способ инкапсуляции стилей, который используется для этого компонента;
exportAs : string[] — имя под которым ссылка на компонент будет доступна в шаблоне;
interpolation : [string, string] — кастомный синтаксис интерполяции, который используются в этом компоненте.

Расположение и вложенность компонентов

Добавить компонент в приложение можно несколькими способами:
— разместив его селектор в шаблоне другого компонента (таким образом достигается вложенность компонентов);
— привязать компонент к определенному маршруту Роутера.

Размещение в шаблоне:

// пользовательский компонент
import { Component } from '@angular/core';

@Component({
  selector: 'films',
  templateUrl: 'films.component.html'
})

export class FilmsComponent {
  constructor() { }
  doSomething() { }
}
// фрагмент шаблона app.component.html
<div class="column wrapper">
    <films></films>
</div>

Привязка компонента к определенному маршруту Роутера.

Для этого необходимо прописать такой компонент в конфигурации Роутера. И затем добавить в app.component.html специальный тег .

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

// фрагмент Роутер модуля с конфигурацией
const routes: Routes = [
  { path: "", pathMatch: "full", redirectTo: "main" }, 
  { path: "main", component: MainComponent },
  { path: "films", component: FilmsComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// фрагмент шаблона app.component.html
<div class="column wrapper">
    <router-outlet></router-outlet>
</div>

Components projection

Components projection и – возможность динамически изменять содержимое шаблона дочернего компонента путем внедрения (проекции) контента из родительского компонента.

Позволяет гибко настраивать компоненты и делает их «reusable».

Родительский компонент

// main.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
  pageName: string = "Film Catalog";
  constructor() { }

  ngOnInit() { }

}
// main.component.html
<div>
<h2>Вас приветсвует приложение о фильмах</h2>
<h3>Перейдите в пункт меню "Фильмы" чтоб увидеть список фильмов</h3>

<details-inner>
    <span><strong>Название главной страницы {{pageName}}</strong></span>
</details-inner>

Дочерний компонент

// details.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'details-inner',
  templateUrl: './details.component.html',
  styleUrls: ['./details.component.css']
})
export class detailsComponent implements OnInit {

  constructor() { }

  ngOnInit() { }

}
// details.component.html
<div>
  <div>Это компонент который показывает детальную информацию</div>
  <div>Ниже конфигурация из родительского компонента</div>
  <ng-content></ng-content>
</div>

ПРАКТИЧЕСКИЕ ЗАДАНИЯ

Все задания можно выполнить с помощью онлайн инструмента StackBlitz или локально, развернув проект самостоятельно используя angular-cli.

  1. Необходимо реализовать 2 компонента. Один называется timeModule второй countdownModule.
    Первый компонент (timeModule) внутри себя должен содержать код, который будет выводить текущую дату и время. На экран должна выводиться следующая информация «Здравствуйте, сегодня 19 мая 2017 года, текущее время 16:25. Время близится к ужину».
    Кроме непосредственно вывода времени вам так же необходимо подставлять одну из трех возможных фраз. Правило следующие. Если текущее время с 21:00 до 9:00 «С утра будет завтрак» если текущее время с 9:00 до 14:00 «Не пропустите обед» если текущее время с 14:00 до 21:00 «Время близится к ужину».
    Второй компонент (countdownModule) должен подключаться в коде первого. То есть, его селектор должен быть в шаблоне первого компонента.
    Второй компонент реализует показ количества дней оставшихся до лета. Например «До лета осталось 15 дней.»

  2. Необходимо создать модуль hello.module, внутри этого модуля компонент — hello.component.
    Внутри компонента определить массив со списком строк:

[
    'Hello World',
    'Привет Мир',
    'Привіт Світ',
    'Hola Mundo',
    'Bonjour le monde',
]

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

Шаблон отображения:
Номер: 1, Значение: ‘Hello Word’
Номер: 2, Значение: ‘привет Мир’

Подключите компонент HelloComponent внутри шаблона корневого модуля app.component.html.

Заготовка для проекта — https://stackblitz.com/edit/angular-start-jse

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

Заготовка для выполнения домашнего задания

  1. В сервисе film.service задан массив объектов films, который содержит набор данных о фильмах.
    Вам необходимо отобразить карточки с этой информацией в компоненте films.component, используя заготовку карточки в примере и разметку GridList из Angular Material. Карточки должны расположится в два ряда по три элемента в ряд.
    Для отображения элементов необходимо воспользоваться конструкцией *ngFor, пример которой находится в app.component.html.

2*) В компоненте main.component разместить компонент Tabs из Angular Material с двумя табами (например: «Информация о портале» и «Дополнительная информация»). В табы поместить произвольный текст.

Варианты решения задач

Задача #1https://stackblitz.com/github/nntndfrk/angular_transformation_1_1

Задача #2https://stackblitz.com/github/nntndfrk/angular_transformation_1_2

TEMPLATES AND DATA BINDING

Взаимодействие компонента с шаблоном

Одна из основных функций компонента является его взаимодействия с шаблоном.

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

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

Затем размещаете эти данные в полях компонента, и в результате можете отобразить их в шаблоне. «Отображение в шаблоне» достигается с помощью механизма property binding.

В свою очередь, с помощью Event Binding вы можете узнать о том, что произошло какое-то событие в коде класса компонента.

Two way binding поможет вам снять значение с input поля, обработать эти данные и передачи далее.

Property binding

“Property binding is a one-way data binding from data source to view target
Property binding или привязка свойств — механизм с помощью которого можно «привязать» значения полей класса компонента (source) в шаблон, директиву или вложенный компонент (target).

Типы привязки свойств (с точки зрения target):
— передача данных свойствам(проперти) DOM элементов
— передача данных во вложенный компонент
— передача данных в директиву


// привязка данных к DOM
<a [href]="website.url" [textContent]="website.linkName"></a> 

// привязка данных к компоненту
<film-card content = "film" [filmId] = "film.id" filmName ="{{film.name}}"></film-card>

// привязка данных к директиве
<p [ngClass]="'one two'" [ngStyle]="obj.value">Example</p> 

Три возможных синтаксиса привязки свойств:

— с помощью интерполяции {{}}
— с помощью квадратных скобок
— с помощью префикса bind-


<film-card [filmId]="film.id" filmName="{{film.name}}" bind-filmYear="film.year"></film-card>

Привязка с помощью интерполяции

“targetProperty = {{expression to evaluate}}
Наиболее популярный способ отображения информации в шаблоне компонента.

Текст между фигурными скобками — это специальное выражение (template expression), которое Angular сначала выполняет, а затем преобразовывается в String.

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


<div>{{'some text'}}</div>
<div>{{filmName}}</div>
<div>{{film.name}}</div>
<div>{{film ? film.name : 'name is not available'}}</div>
<div>{{filmName + getActorName()}}</div>

Особенности интерполяции

— контекстом выполнения является компонент
— вы не можете получить доступ к window, console.log и др.
— нельзя делать некоторые присвоения (+=, -=)
— нельзя выполнять побитовые операции
Так же внутри {{}} вы можете обращаться к template input variable (let film) и template reference variable (#filmItem).

// In component
let filmList = [
    {name: "Matrix"},
    {name: "Resident Evil"}
]
// In template
<div *ngFor="let film of filmList">{{film.name}}</div>

<input class="small-input" #filmItem> {{filmItem.value}}

Привязка свойств с помощью []

“[targetProperty] = «expression to evaluate»;
Во многом эти два синтаксиса схожи. Интерполяцию {{}} чаще всего используют для вывода значений типа string непосредственно в шаблон, а [] для передачи в target property более сложных структур (например объектов).

Другими словами, если результат вычисления string разницы нет ([] или {}), если что то другое, необходимо использовать [].

<img src="{{filmImageUrl}}">
<img [src]="filmImageUrl">

<div><span>{{filmName}}</span> отличный фильм</div>
<div><span [innerHTML]="filmName"></span> отличный фильм</div>
<div><span [innerText]="filmName"></span> отличный фильм</div>

Привязка свойств с помощью bind-

“bind-targetProperty = «expression to evaluate»;
Такой синтаксис является аналогичным [] и используется значительно реже.

var filmImageUrl = "http://test.com/image1.png"
var filmName = "Resident Evil"

<img src="{{filmImageUrl}}">
<img [src]="filmImageUrl">
<img bind-src="filmImageUrl">

<div><span>{{filmName}}</span> отличный фильм</div>
<div><span bind-innerHTML="filmName"></span> отличный фильм</div>

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

Logo: <img [src]="website.logo"/>
Logo: <img bind-src="website.logo"/>
Logo: <img src="{{website.logo}}"/>

Url: <a [href]="website.url" [textContent]="website.name"> </a>
Url: <a bind-href="website.url" bind-textContent="website.name"> </a>
Url: <a href="{{website.url}}" textContent="{{website.name}}"> </a>


<p [ngClass]="'one two'"> Angular Property Binding Example </p>
<p bind-ngClass="'one two'"> Angular Property Binding Example </p>
<p ngClass="{{'one two'}}"> Angular Property Binding Example </p>

<p [ngClass]="one two"> Angular Property Binding Example </p> //ERROR
<p ngClass="one two"> Angular Property Binding Example </p> //ERROR

//prefixMsg = "Website name is" возможно если вам необходимо просто передать не изменяемое
// стринговое значение внутрь компонента
 
<film-card prefixMsg = "Website name is" [siteName] = "website.name"> </film-card> 
<film-card prefixMsg = "Website name is" bind-siteName = "website.name"> </film-card>
<film-card prefixMsg = "Website name is" siteName = "{{website.name}}"> </film-card> 

Разница между HTML attributed и DOM (Document Object Model) properties.

Следует понимать, что вычисленное выражение присваивается именно property DOM объекта, а не атрибуту.

Разница между атрибутами и пропертями.

— Некоторые HTML атрибуты совпадают 1:1 с пропертями. ID, value один из таких примеров.

— Некоторые HTML атрибуты вообще не имеют соответствующих проперти. Сolspan один из таких примеров.

— Некоторые DOM проперти не имеют соответствующих атрибутов. Например textContent или innerHtml.

“Атрибуты инициализируют DOM проперти и на этом все (они неизменны). А проперти в дальнейшем могут изменяться.

//Создаем input 
//в DOM ноде устанавливаем значение property value в "Hello"
// значение берется из соответствующего атрибута
<input type="text" value="Bob">

Установка атрибутов напрямую

Работа с атрибутами в Angular не рекомендуется.

Но если все таки необходимо установить атрибут, это можно сделать следующим образом.
“[attr.attrName]=»expression»

<td [attr.colspan]="getColspan()">table cell</td>

Class and style

Так же у вас есть возможность устанавливать классы и стили. Но для работы с атрибутами class и style лучше использовать специальные директивы ngClass и ngStyle.

<div [class.active]="!isActive">This one is not so special</div>

<button [style.background-color]="isError() ? 'red': 'grey'" >Сохранить</button>

Event binding

Мы рассмотрели как установить значение из класса компонента в его шаблон, вложенный компонент или директиву.
Для реакции на действия пользователя используется event binding или «привязка событий».

Этот механизм который позволяет:

— обработать стандартные события браузера, такие как click, keyup, focus;
— передать значение из вложенного компонента во внешний.

<button (click)="saveSettings()">Сохранить изменения</button>
<button on-click="cancel()">Отменить</button>

“(templateEvent) = «templateStatement»
Слева в круглых скобках указывается имя события, на которое необходимо отреагировать.

Справа — выражение, которое будет запущено (вычислено) во время наступления этого события.

<button (click)="saveSettings()">Сохранить изменения</button>
<input (keyup.enter)="sendMessage()">
<button on-click="cancel()">Отменить</button>

Чаще всего в качестве выражения выступает функция. Хотя теоретически выражение может быть любым (рассмотрим далее).

$event

Выражение, которое вычисляется после наступления события, может принимать объект $event. Этот объект содержит информацию о наступившем событии.
Если наступившее событие является «классическим» DOM событием, то значением $event тоже является оригинальный объект event.

<button (click)="saveSettings($event)">Сохранить изменения</button>

// установка значения поля компонента после наступления события (v1)
<input [value]="film.name"
       (input)="film.name=$event.target.value" >


// установка значения поля компонента после наступления события (v2)
<input [value]="film.name"
       (input)="setName($event)">

setName(event) {
    film.name = $event.target.value;
}

// установка значения поля компонента после наступления события (v3)
<input [(ngModel)]="film.name">

EventEmitter

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

Рассмотрим как работает это механизм.

// шаблон вложенного компонента
<div>
  <img src="{{film.url}}">
  <span>
    {{film.name}}
  </span>
  <button (click)="edit(film.id)">Редактировать</button>
</div>

// Код вложенного компонента
export class FimCardComponent {
  constructor() { }

  editEvent = new EventEmitter();

  edit(filmId) {
    this.editEvent.emit(filmId);
  }

}


// Фрагмент кода внешнего компонента
<film-card (editEvent)="updateParent($event)" [film]="selectedFilms"></film-card>

Two way data binding

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

— отображать данные в шаблоне если они изменились в коде класса компонента;
— автоматически получать измененные значения из шаблона в код компонента.

Наиболее часто используется с формами (input полями).

// использование двухстороннего связывания для работы с полем ввода
<input placeholder="Название фильма" [(ngModel)]="filmName" (keyup.enter)="findFilm()">

Декораторы @Input и @Output

Часто бывает необходимо передать данные внутрь компонента, или наоборот «пробросить» данные из внутреннего компонента в компонент верхнего уровня.
Для этого используются декораторы @input и @output. Они указывают компоненту, какие проперти должны быть переданы в компонент извне, а какие наоборот можно возвращать наружу.

Декораторы обычно указываются на самих полях класса (property decorators)

  @Input() filmName: string = "default name"; 

  @Input("length") filmLength: number = 256;

  @Output() ev: EventEmitter = new EventEmitter();

Хотя доступен и альтернативный вариант

@Component({
  inputs: ['filmNmae'],
  outputs: ['ev'],
})

Допустим у нас есть компонент верхнего уровня FilmsComponent, в его шаблоне внедряем дочерний компонент DetailsInnerComponent

<div>
    <details-inner [info]='description'></details-inner>
</div>

info — это название поля класса внутреннего (дочернего) компонента DetailsInnerComponent

description — это поле класса текущего (внешнего) компонента FilmsComponent
Для того чтоб внутренний компонент смог получить параметр, необходимо во внутреннем компоненте прописать специальный декоратор @Input

// класс
export class DetailsComponent implements OnInit {
  @Input() info: string 
  constructor() { }
  ngOnInit() {  }
}
// шаблон
<div>
  <div>Input {{info}}</div>
</div>

Для передачи значения в компонент верхнего уровня используется следующая механика:
— в дочернем компоненте создаем поле класса с декоратором @Output
создаем в этом поле EventEmitter
— когда нужно, вызываем метод emit и передаем параметр
— в родительском компоненте с помощью механизма EventBinding «прослушиваем» этот параметр и вызываем соответствующий метод родительского компонента
— в методе класса родительского компонента выполняем все необходимые действия

// дочерний класс
export class DetailsComponent implements OnInit {
  @Input() info: string 
  @Output() update = new EventEmitter<string>();
  value: string;
  constructor() { }

  setToParent(){
    this.update.emit(this.value);
  }
}
// шаблон дочернего класса
<div>
  <div>Input {{info}}</div>
<button (click)="setToParent('text msg')">Перенести в родительский</button>
</div>

// шаблон родительского класса
<details-inner (update)="setUpdatedValue($event)"></details-inner>

//родительский класс
export class FilmsComponent  {
  aditionalTitle: string;
  constructor() {  }

  setUpdatedValue(eventParam){
    this.aditionalTitle = eventParam;
  }
}

Работа с конструктором класса

Есть 2 основных способа назначить поля класса:
— классический способ — просто прописываете поля класса

export class FilmComponent {
  
    filmId: number;
    filmName: string;

    doSomething() {
        console.log(this.filmName);
    }
}

— указать поля класса в конструкторе, передав их как аргументы и добавив модификатор public

export class FilmComponent {

    constructor (
        public filmId: number,
        public filmName: string
    ) { }
}
  1. Создайте шаблон с кнопкой и текстом «Изменить цвет». По нажатию на кнопку необходимо изменить значение проперти компонента «isPressed» с false на true. В шаблоне добавить div с текстом «Цвет изменен». В стилях добавить
    Один css класс, где прописан красный цвет текста. Используя конструкцию [class.className] или ngClass устанавливаете на div класс redText в случае если «isPressed» == true. При повторном нажатии текст должен стать обычного цвета. Задачу выполнить в StackBlitz или аналоге.

  2. Создайте родительский компонент и вложенный в него дочерний компонент. В родительском компоненте должен быть div с текстом «Текст по умолчанию». Дочерний компонент должен содержать кнопку при нажатии на которую, через event binding передается событие в родительский компонент. Кроме события также передается строка «Текст из дочернего компонента». При этом, текст из родительского компонента должен быть заменен на тот, который мы передали из дочернего. Задачу выполнить в StackBlitz или аналоге.

Варианты решения задач

Задача #1https://stackblitz.com/github/nntndfrk/angular_transformation_2/tree/hw_1

Задача #2https://stackblitz.com/edit/3-3

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

Заготовка для выполнения домашнего задания

В этом домашнем задании Вам необходимо сделать следующее.

  1. Весь элемент <mat-card> с прошлого домашнего задания, который соответствует одной карточке переместить внутрь нового компонента <film-item>, а компонент <films> переименовать (пересоздать) как компонент <film-list>(или <films-list>). Сделать это таким образом, чтобы компонент <film-item> был дочерним компонентом <film-list>(или <films-list>).
    В родительском компоненте с помощью *ngFor вывести дочерние <film-item> и передать в них через property binding все данные, которые необходимые для отображения фильма. Эти данные нам доступны из нашего сервиса FilmService.

2*) В заготовке уже есть готовая примерная верстка элемента с селектором из библиотеки Angular Material, который позволяет выбирать тип сортировки карточек с фильмами. Вам необходимо разобраться, как работает селектор и реализовать сортировку в алфавитном порядке по названию карточек с фильмами.

  1. Реализовать механизм передачи события с вложенного компонента во внешний (родительский), как было показано на сессии. В заготовке у Вас будет рабочий пример.
    Вам необходимо реализовать следующее. При нажатии на кнопку “Добавить в избранное” на карточке с фильмом с помощью механизма event binding передать данные на компонент верхнего уровня <film-list>(или <films-list>) и в нем обновить счетчик, который будет отображать общее число добавленных в избранное.

ДИРЕКТИВЫ

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

В Angular входит определенное количество встроенных директив, которые помогают решать множество типовых задач. Все они тем или иным способом взаимодействуют с шаблоном (DOM структурой).

Директивы бывают трех видов:

  • компонент — это директива, которая обладает собственным шаблоном;
  • структурные директивы — изменяют DOM структуру, добавляя или удаляя элементы;
  • директивы атрибутов — меняют внешний вид или поведение элементов, компонентов или других директив.

Attribute directives

“An Attribute directive changes the appearance or behavior of a DOM element
Меняют отображение или поведение DOM элементов, устанавливают значения в атрибуты HTML элементов и др.

Называются так, потому что во многом похожи на обыкновенные HTML атрибуты.
Рассмотрим три основные директивы:

  • NgClass — добавляет и удаляет CSS классы;
  • NgStyle — добавляет и удаляет инлайн стили;
  • NgModel — подключает двухстороннее связывание для форм.

NgClass

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

<div class="film-name text-big"
     [class]="text-small">Star Wars</div>

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

<div class="film-name"
     [class.red-color]="isTextRed()">Star Wars</div>

Этот подход можно использовать в случае необходимости установить/удалить один класс.

Если необходимо работать с несколькими классами — лучше воспользоваться директивой NgClass .

Директива позволяет задавать пары ключ:значение.
Где ключ это имя класса, а значение это вычисляемое выражение.
Если выражение равно

true

— класс будет применен, если выражение равно

false

— класс будет удален.

<div class="message"
          [ngClass]="{ 'from': message.author === 'bot',
                       'to':   message.author === 'user',
                       'red-color': isColorRed() }">
    {{ message.content }}
</div>

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

<div class="message"
          [ngClass]="filmClassConfig">
    {{ message.content }}
</div>
this.filmClassConfig = {
    'from': this.message.author === 'bot',
    'to':   this.message.author === 'user',
    'red-color':  this.isColorRed()
};

NgStyle

Директива позволяет устанавливать инлайн стили.

Следует напомнить, что в самом простом случае вы можете установить стиль с помощью так называемого «style binding».

<button [style.background-color]="isRed ? 'red': 'green'" >Сохранить</button>
<span [style.font-size.px]="isBigFont ? 23 : 12" >Черная пантера</span>

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

Необходимо задать пары ключ:значение.

Где ключ — это название стиля, а значение — специальное выражение. Результатом вычисления выражения будет само значение стиля в виде string , которое необходимо применить.

<div class="message" [ngStyle]="stylesConfig">
    {{ message.content }}
</div>
isVisible: boolean = true;

this.stylesConfig = {
    'display':  this.isVisible ? 'block' : 'none',
    'color':    this.getTextColor()
  };

NgModel

Специальная директива, которая позволяет реализовать двухстороннее связывание для элементов формы (например input, textarea).

Значения введенные пользователем в input поле автоматически попадают в код компонента и наоборот. Любое вычисленное в коде выражение будет автоматически отображено в шаблоне и видно пользователю.

<input [(ngModel)]="film.title">
<textarea[(ngModel)]="film.description"></textarea>

Необходимо ​понимать, что такой результат можно достигнуть и другим способом:

<input [value]="film.title"
       (input)="film.title=$event.target.value">

В этом случае одновременно используется property binding и event binding. Однако, следует знать, что в реальности такой подход не используется.

Синтаксис [(ngModel)]=’fieldName’ позволяет вам только обновлять значение поля fieldName.
Если вам необходимо выполнять разные действия в зависимости от направления связывания, вы можете воспользоваться следующим подходом.

<input
  [ngModel]="film.title"
  (ngModelChange)="setFilmName($event)">

[ngModel] реализует property binding.
Переносит значение film.title в input поле.
(ngModelChange) реализует event binding.

Метод setFilmName() может не только сохранить значение из input в код компонента, но и выполнить любую другую операцию.

Использование двухстороннего связывания для передачи параметра в компонент.

Теоретически есть возможность передавать параметр внутрь компонента с помощью двухстороннего связывания. Однако этот способ практически не имеет никаких преимуществ перед «классическим» способом.
Рассмотрим это на примере:

// шаблон родительского компонента
<film-item [(filmInfo)]="film"></film-item>
//код дочернего компонента
@Component({
  selector: 'film-item',
  templateUrl: 'app/film-item.component.html'
})
export class FilmItemComponent {
  @Input() filmInfo: object;
  @Output() filmInfoChange: EventEmitter<object>;

  filmInfoChange = new EventEmitter<object>();
}

Built-in structural directives

“Structural directives are responsible for changing HTML layout. By adding, removing, and manipulating the host elements to which they are attached.
Меняют HTML структуру в целом, путем добавления или удаления элемента на котором они прописаны.
Тот элемент к которому применяется структурная директива называется «хост» элементом (host element).

Структурная директива может быть добавлена только одна в отличии от директивы атрибутов. Их может быть несколько.

Три наиболее популярные структурные директивы:

  • NgIf — добавляет и удаляет хост элемент из DOM структуры;
  • NgSwitch — — позволяет выбрать из нескольких альтернативных видов отображения;
  • NgForOf повторяет шаблон для каждого элемента коллекции.

NgIf

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

Сначала вычисляется выражение справа. Если результат выражения true — хост элемент будет показан. Если результат выражения false — хост элемент не будет отображен.

// компонент
<film-item *ngIf="isFilmAvailable"></film-item>

// HTML код
<div class="film-info" *ngIf="getVisibilityStatus()">StarWars</div>

Один из популярных сценариев использования.

// вариант 1
<div class="film-info">{{film.name ? film.name : 'фильм не найден'}}</div>

// вариант 2
<div class="film-info" *ngIf="film.name">{{film.name}}</div>
<div class="film-info" *ngIf="!film.name">фильм не найде</div>
NgIf удаляет или скрывает элементы?

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

Но если речь идёт о целом компоненте, то просто спрятать его плохая идея. Он продолжит работать, обработчики событий продолжают занимать память и т.д.

Зачем в ngIf звездочка (*ngIf)?

Звездочка это так называемый “синтаксический сахар”. Angular трансформирует *ngIf атрибут в элемент <ng-template>, который оборачивает хост элемент.

//оригинальная запись
<div class="film-info" *ngIf="film">{{film.name}}</div>

// преобразование * в <ng-template>
<ng-template [ngIf]="film">
  <div class="film-info">{{film.name}}</div>
</ng-template>

Таким образом, мы получаем обыкновенный property binding.

В процессе рендеринга элемент <ng-template> будет удален.
Подобный механизм работает и для директив *nfFor, *ngSwitch.

Продвинутые возможности

В самом простом сценарии применения директивы ngIf сначала вычисляется выражение и в зависимости от этого показывается или нет хост элемент.

Необходимо знать, что директива поддерживает и более продвинутый синтаксис. Давайте рассмотрим несколько примеров.
Использование else . Блок «если» подключается из отдельного шаблона.

<div *ngIf="film; else noFilmsBlock">{{film.name}}</div>
<ng-template #noFilmsBlock>Нет фильмов для отображения</ng-template>

Блок «тогда» и «если» подключается из отдельного шаблона.

<div *ngIf="film; then filmInfoBlock else noFilmsBlock"></div>
<ng-template #filmInfoBlock>{{film.name}}</ng-template>
<ng-template #noFilmsBlock>Нет фильмов для отображения</ng-template>

Назначение алиаса.

<div *ngIf="getCurrentFilm() as film; else noFilmsBlock">{{film.name}}</div>
<ng-template #noFilmsBlock>Нет фильмов для отображения</ng-template>

NgSwitch

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

Механика работы во многом похожа на классический оператор switch из JavaScript.

В реальной работе используется не очень часто.

// выбираем какой компонент показывать
<div [ngSwitch]="user.status">
  <admin-info   *ngSwitchCase="'admin'"></admin-info>
  <student-info *ngSwitchCase="'student'"></student-info>
  <user-info    *ngSwitchCase="'user'"></user-info>
  <user-info    *ngSwitchDefault></user-info>
</div>

// выбираем какой div показывать
<div [ngSwitch]="user.status">
  <div *ngSwitchCase="'admin'">Hi, Administrator</div>
  <div *ngSwitchCase="'student'">Hi, Student</div>
  <div *ngSwitchCase="'user'">Hi, User</div>
  <div *ngSwitchDefault>Hi, User</div>
</div>

NgFor

Позволяет отображать коллекции, например массивы.

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

// выбираем какой компонент показывать
<film-details *ngFor="let film of films" [filmInfo]="film"></film-details>

// выбираем какой div показывать
<div *ngFor="let film of films">{{film.name}}</div>
Расширенный синтаксис NgFor

Выражение, которое находится справа от ngFor называют «микросинтаксис». Кроме непосредственно элемента на каждой итерации цикла вы так же можете:

  • получить порядковый номер элемента (index)
  • идентифицировать первый элемент коллекции (first)
  • идентифицировать последний элемент коллекции (last)
  • идентифицировать четный/нечетный элемент коллекции (even/odd)
<ul>
    <li *ngFor="let film of films; index as i; first as isFirstItem; last as isLastItem">
        <div *ngIf="isFirstItem">Первая строка</div>    
        {{i}}/{{films.length}}. {{film.name}} 
        <div *ngIf="isLastItem">Последняя строка</div>
    </li>
</ul>
trackBy

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

@Component({
  selector: 'my-app',
  template: `
    <ul>
      <li *ngFor="let item of collection;trackBy: trackByFn">{{item.id}}</li>
    </ul>
  `,
})
export class App {

  constructor() {
    this.collection = [{id: 1}, {id: 2}, {id: 3}];
  }
   
  trackByFn(index, item) {
    return item.id;
  }
}
<ng-container>
Структурная директива обычно находится на хост элементе. Бывают ситуации когда использовать хост элемент не удобно. В этом случае вы можете воспользоваться специальным тегом <ng-container>
<p>
    Приветствую вас,
  <ng-container *ngIf="isAdmin">
    главный админ
  </ng-container>
  как ваши дела? хорошо?
</p>

<p>
  <ng-container *ngIf="isAdmin">
    <span>Создать</span> | <span>Удалить</span>
  </ng-container>
  Описание фильма.......
</p>

Он не внедряется в шаблон. Просто является некой «оберткой», которая будет удалена в результирующем HTML.

  1. Создайте шаблон с тремя кнопками («показать первую картинку», «показать вторую картинку», «показать все картинки») и спрятанными изображениями. При помощи директивы ngClass и property binding сделайте так, чтобы при нажатии на первую кнопку показывалось первое изображение, при нажатии на вторую показывалось второе изображение, при этом первое исчезало. Третья кнопка должна показывать оба изображения при нажатии на нее. Задачу выполнить в StackBlitz или аналоге.

  2. Дано массив с данными.

Создайте компонент phone-shop.
Массив с данными необходимо вывести в виде карточек с данными в компоненте phone-shop, в карточках отобразить название-модель (phone.name — phone.model), описание (description), остаток “на складе” (qty).
При “клике” на кнопку “Купить” в карточке, увеличивать значение переменной “корзина”, которая будет отображать количество товаров в “корзине” в компоненте phone-shop (место отображения в компоненте на Ваше усмотрение) и уменьшать значение qty для элемента массива, по отображению которого произвели “клик”.
Если количество (поле “qty”) в значении элемента станет равным 0, сделать кнопку “Купить” в карточке неактивной. Надпись «на складе» выделить красным цветом и ниже ее вывести надпись “Ожидайте поступления!”.

Варианты решения задач

Задача #1https://stackblitz.com/github/nntndfrk/angular_transformation_2/tree/hw_2

Задача #2https://stackblitz.com/edit/4-2

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

Задание
  1. Хранить информацию о том, находиться ли фильм в избранном, в объекте фильма. Создать для этой информации отдельное поле (булевого типа) в объекте фильма. Нескольким фильмам должно быть заранее проставлено информацию, что фильм в избранном.
  2. Добавить в toolbar приложения input-поле, с помощью которого можно будет осуществлять поиск по имени фильма. Ограничить длину строки для запроса не менее чем в три символа. Данные при поиске должны поступать с сервиса. Если результаты поиска есть — отображаете их, если нет — поставить “заглушку”, о том что фильмы не найдены.
  3. Отдавать с сервиса только часть (например половину) данных при первом обращении. Внизу карточек с фильмами поместить по центру кнопку “Загрузить еще”, при нажатии на которою погрузиться новая “порция” данных с сервиса. Если данных на сервисе больше нет, сделать кнопку “Загрузить еще” неактивной. Желательно учесть при получении новых данных порядок сортировки в нашем текущем отображении.

Новый список фильмов

COMPONENT ADVANCED

Lifecycle hooks

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

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

Фактически вы описываете методы класса, которые выполнятся в тот или иной момент жизненного цикла.

Использовать все хуки сразу нет никакой необходимости. Чаще всего используется несколько наиболее популярных.

// выполняется один раз при инициализации директивы
ngOnInit(){}

// выполняется один раз при уничтожении компонента
ngOnDestroy(){}

// запускается когда меняются значение привязанных проперти
ngOnChanges()
Рассмотрим более детально некоторые из них:

ngOnChanges() Вызывается, в момент когда Angular устанавливает или обновляет “привязанные” к компоненту свойства ([], {{}}).
В качестве аргумента метод получает объект SimpleChanges с текущими и предыдущими значениями свойств.
В начале вызывается перед ngOnInit() и каждый раз, когда одно или несколько значений data-bound свойств изменены.
ngOnInit() Вызывается после инициализации директивы/компонента (все свойства переданные в компонент уже получены).
Вызывается один раз при создании компонента после ngOnChanges().
ngAfterContentInit() Вызывается после того как Angular помещает контент в представление дочернего компонента ( сontent projection )
ngAfterViewInit() Вызывается после того как Angular инициализирует компонент и его дочерние компоненты.
ngOnDestroy() Вызывается перед тем, как Angular уничтожает директиву/компонент. Внутри необходимо реализовывать отписку от наблюдателей и обработчиков событий, что бы избежать утечек памяти.

Декоратор @ViewChild

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

@ViewChild(ComponentName)
comp: ComponentName;

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

<div #name>Название: <span [innerText]="filmName"></span></div>

...

@ViewChild("name", { read: ElementRef })
nameDiv: ElementRef;
this.nameDiv.nativeElement.innerHTML = "";

ElementRef

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

@ViewChildren

Используется если необходимо получить доступ к списку компонентов.

@ViewChildren(ComponentName)
comp: QueryList<ComponentName>;

QueryList это класс, который поддерживает работу со списками компонентов. В частности содержит методы map(), filter() , find(), reduce(), forEach(), some().

Получение ElementRef в конструкторе

Вы можете получить ссылку на корневой DOM элемент компонента в конструкторе. Сделать это можно следующим способом.

@Component({
    selector: 'film-test',
    ...

export class FilmTest {
    constructor(private hostElement: ElementRef) {
    console.log(this.hostElement.nativeElement.outerHTML);
}

Более детально использование @ViewChild и @ViewChildren рассмотрено в специальном примере, который прикреплен к сессии.

Взаимодействие компонентов (схемы)

Передача данных в Child

Передача данных в Parent

@ngOnChanges()

@ViewChild

Service

Вам необходимо сделать 3 компонента. Один родительский, и два дочерних.
В одном из дочерних компонентов находится три кнопки radio button (значения: маленький шрифт, средний шрифт, большой шрифт), а также selectbox с тремя значениями (красный, синий, зеленый).
Второй вложенный компонент содержит проперти ‘msg’ со значением «Текст дочернего компонента». Проперти прописана в шаблоне с помощью интерполяции. Соответственно этот текст виден на экране.

Задача 1. При выборе одного из radio button с помощью event bindin эмитить событие из дочернего компонента в родительский. Затем с помощью механизма @ViewChild вызывать напрямую в другом дочернем компоненте метод который будет изменять размер шрифта. Для изменения размеров шрифта вам необходимо подставлять один из трех css классов. Классы менять с помощью директивы ngClass.

Задача 2. В первом вложенном компоненте выбираем цвет текста с помощью selectbox. Передаем событие наверх с помощью event binding. Второй дочерний компонент с помощью хука ngOnChanges() автоматически определяет изменение проперти родительского компонента и меняет цвет шрифта.
Задачу реализовать в StackBlitz или аналоге.

Варианты решения задач

Задача #1, #2https://stackblitz.com/github/nntndfrk/angular_transformation_5

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

Задание
  1. Необходимо при первой загрузке страницы с фильмами получить список фильмов с внешнего api, воспользовавшись методом сервиса, реализация которого дана в заготовке. В элементе фильма отобразить информацию: название, дата релиза, рейтинг, картинку (постер), описание и остальное на усмотрение. Длину описания сократить для корректного отображения.
  2. В селекторе (который отвечал в нас за сортировку раньше) реализовать два пункта: “Фильмы” и “Актеры”. Выбор “Фильмы” будет отвечать за отображение фильмов и этот вариант будет по умолчанию. Выбор “Актеры” должен отобразить популярных актеров. Эти данные необходимо получить с внешнего api, по аналогии с фильмами.
  3. Переделать функционал пейджинга (догрузки) для корректной работы через внешний api. Реализацию сортировки в этом варианте необходимо убрать.
  4. Функционал поиска перенести в отдельный компонент и реализовать механизм его взаимодействий с родительским компонентом, что бы функционал поиска остался рабочим. В данной реализации поиск будет работать только с данными, которые уже есть в компоненте.
  5. Реализовать UI-элемент лоадера, который будет отображаться на экране, пока ожидаются данные с внешнего api. Для этой задачи может подойти обычная gif-анимация лоадера или предназначенные для этого компоненты (Progress spinner, Progress bar) из библиотеки Angular Material.

Заготовка для выполнения домашнего задания

Главная документации api

Популярные фильмы

Популярные актеры

TYPESCRIPT OVERVIEW

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

В этой теме мы узнаем, что такое TypeScript. Научимся устанавливать, настраивать и запускать TypeScript в стороне от Angular приложений. Разберемся с понятием типизации. Посмотрим на классы и наследование в TypeScript, а также их сходства и отличия от классов и наследования в ES6. Завершим тему коротким обзором generic-ков и модулей в TypeScript.

Что такое TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

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

Появился в конце 2012 года в компании Microsoft. Тем не менее является некоммерческим OpenSource проектом. Одними из первых, кто начал использовать TypeScript була команда разработчиков Angular. Хотя в самом начале разработки Angular существовало две версии, на TS и на обычным JavaScript, но со временем осталась только одна, официально поддерживаемая версия на TypeScript. Так же стоит отметить, что у других фреймворков (Vue, React) тоже появляются TypeScript версии, это говорит о большой популярности технологии.
На данный момент браузеры не умеют исполнять TS, по этому любой TS код должен быть преобразован в обычный JS.
Многие возможности TS постепенно внедряются в обычный JS, например классы, но многие еще нет (статическая типизация, интерфейсы).

Рассмотрим некоторые преимущества и недостатки TypeScript:
Преимущества:

  • — строгая типизация (подсказки в IDE, обнаружение ошибок на стадии компиляции)
  • — более “точно” следует принципам ООП чем обычный JS
  • — любой код JS автоматически является кодом TS
  • — более похож на такие языки как Java/C#
  • — другие фреймворки тоже внедряют возможности использования TS
  • — используется Angular как основной язык

Недостатки:

  • — необходимо компилировать в чистый JS
  • — необходимо изучать дополнительный синтаксис

Как видим, преимуществ при использовании TypeScript гораздо больше чем недостатков.

Запуск и конфигурация TypeScript

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

  1. npm install -g typescript

В случае Angular проекта сам TypeScript уже установлен и компиляция TS в JS происходит автоматически на этапе сборки.
Вручную компиляцию можно запустить двумя способами:

  1. tsc demo.ts

или

  1. tsc -w demo.ts

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

Задать параметры для компиляции можно двумя способами:

  • — непосредственно указывая специальные параметры в консоли
  • — добавив в проект специальный файл tsconfig.json

В реальной разработке всегда более предпочтительный второй вариант.
Если открыть существующий Angular проект сгенерированный с помощью cli, то можно обнаружить в корне проекта такой файл конфигурации TypeScript.
Справочник по конкретным значения параметров находится здесь.

Типизация

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

  1. class property | variable | argument | function : type

поле класса, переменная, аргумент, объявление функции затем двоеточие и тип.

Различные варианты указания типов можно посмотреть на примере:

let name: string;
let name: string = "Anakin Skywalker";

function getName (name: string) {
    return "Hello, " + name;
}

function getName (name): string {
    return "Hello, " + name;
}

function getName (name: string): string {
    return "Hello, " + name;
}

getName(name);

Типы данных в TypeScript

В TypeScript существуют следующие типы данных:

Boolean true или false
Number числа
String строки
Array массивы
Object объекты
Tuple кортежи (упорядоченный набор фиксированной длинны)
Enum перечисления
Any любой тип
Void тип не задан

Типы Boolean, Number, String, Array, Object точно такие же как и в обычном скрипте.
Если переменной назначен тип, то установить значение другого типа невозможно.

var userName: string = "john";
userName = 15; // ошибка 

Если тип явно не указан, то считается что переменная типа any, в таком случае ее поведение точно такое же как и в обычном скрипте.

Array

Массивы записываются следующим образом

let items: number[] = [10, 20, 30];
let itemNames: string[] = ["John", "Connor", "Anakin"];

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

let items: Array = [10, 20, 30];
let itemNames: Array = ["John", "Connor", "Anakin"];

Взаимодействие с массивом происходит так же, как и JavaScript, с тем отличием, что внутрь массива можно помещать только значения заданного типа.

Tuples

Задают массивы определенной длинны с заранее прописанным типом данных на каждой позиции

let items: [number, number, number] = [10, 20, 30];
let itemNames: [number, string] = [15, "Anakin"];

Enum

Списки заранее определенных значений

enum Status {
    Open,
    Inprogress,
    Ready,
    Closed,
    Reopen = 15
}

//.........

if (status == Status.Open) {
    // some code
}

Any

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

let items: any = [10, 20, 30];
let items = "John";

Void

Отсутствие любого возможного типа.
В переменных используется редко. Таким переменным можно назначить только null и undefined.
Используется в функциях если функция не возвращает ничего.

function showInfo(): void {
    console.log("This is info message");
}

Union (Объединение типов)

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

let twice: (string | boolean);

Alias

С помощью ключевого слова type можно задать псевдоним составного типа.

type dobletype = string | boolean
let twice: dobletype = true;

Функции

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

function isAdmin (userName: string, isLoggedIn: boolean): string {
    var result = "yes";
    // some logic
    return result;
}

function isLoggedIn (userName: string, tokens: number[] = [15,25]): boolean {
    var result = true;
    // some logic
    return result;
}

function isLoggedIn (userName: string, token?: number[]): boolean {
    var result = true;
    // some logic
    return result;
}

Интерфейс функции

Возможно установить интерфейс функции, то есть какие аргументы у нее могут быть и тип возвращаемого значения.

let getName: (name: string, age: number) => string;
getName = function(name, age){
    return name + "age " + age;
};
console.log(getName("John", 20));
getName = function(name, age){
    return "Hello, " + name + age;
};

console.log(getName("John", 20));

Конструкцию можно усложнить, если один из аргументов в свою очередь то же функция.

let getFullName: (name: string, age: number, getSecondName: (isNeeded: boolean) => string) => string;

let secondName = function(isNeeded){
    return isNeeded ? " Smith" : "";
};

getFullName = function(name, age, getSecondName){
    return name + "age " + age + getSecondName(true);
};
console.log(getFullName("John", 20, secondName));

Классы и наследование

TypeScript реализует классические принципы ООП. Синтаксис классов очень похож на реализацию ES6. Класс объявляется с помощью ключевого слова class.

class UserSetting {
    id: number
    name: string
    status: boolean
    settingList: any

    constructor () { }

    setId(id: number): void { this.id = id }
    getId(): number { return this.id }

    getLastSetting(): Array<Setting> {}
}

Класс состоит из полей класса, методов и конструктора.
К характерным особенностям классов в TypeScript от ES6 можно отнести то, что поля класса объявляются не внутри функции-конструктора, а вначале класса за пределами любых методов. Также существует сокращенный синтаксис объявления полей класса в функции-конструкторе, на котором остановимся чуть позже.
Конструкторы вызываются автоматически при создании класса. Основная их роль получить параметры и/или установить начальную конфигурацию объекта, который создается.

class UserSettings {
    id: number
    name: string
    status: boolean

    constructor (name: string, status: boolean) {
        this.name = name
        this.status = status
    }
    
    setId(id: number): void {this.id = id}
    getId(): number {this.id;}
    
}

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

  • public — доступен публично (устанавливается по умолчанию)
  • protected — доступен наследникам
  • private — недоступен извне
class UserSettings {
    id: number;
    name: string;
    private status: boolean;

    constructor (name: string, status: boolean) {
        this.name = name
        this.status = status
    }
    
    setId(id: number): void {this.id = id}
    getId(): number {return this.id;}   
}

let userSettings = UserSettings("John", true);
userSettings.status;

Благодаря модификаторам доступа вы можете значительно сократить запись конструктора класса.

class DefaultSettings extends Settings {
    userRole: string = 'admin';

    constructor (public id: number, public status: boolean, private settingName: string) {
        super(id, status);
    }
}

let defaultSettings = new DefaultSettings(15, true, "testName");
console.log(userSettings.settingName);

Рассмотрим детальнее. Если прописать в конструкторе (точнее в его аргументах) на каждый из аргументов модификатор доступа public, то автоматически будут созданы соответствующие аргументам поля класса и им будут присвоенные те значения, которые будут переданными параметрами в функцию-конструктор при создании класса.
Если смотреть на примере, то у класса DefaultSettings кроме публичного поля userRole будут существовать еще поля публичные поля status и id с соответствующими значениями. Поле settingName не будет автоматически созданное как публичное поле, поскольку ему задано модификатор private.

Также как и в ES6, в TypeScript работает механизм наследования.
Наследование реализуется с помощью ключевого слова extends.

class Settings {
    id: number;
    status: boolean;

    constructor (id: number, status: boolean) {
        this.id = name
        this.status = status
    }
    
    setId(id: number): void {this.id = id}
    getId(): number {this.id;}
    getStatus(): boolean {
        return this.status;
    }
}

class UserSettings extends Setting {
    settingName: string;
    userRole: string = admin;

    constructor (id: number, status: boolean, settingName: string) {
        super(id, status);
        this.settingName = settingName;
    }
    
    getStatus(): string {
        let status = super.getStatus()
        this.userRole !== 'admin' ? "User is Admin" : "User is default user";   
    }
}

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

Интерфейсы

Объекты, которые содержат описание полей но не содержат реализацию.
Позволяют создать свой «тип данных» и в дальнейшем его использовать.

interface SettingItem {
    name: string,
    active: boolean,
    value: number,
    alias?: string
}

let mySetting: SettingItem = {
    name: "default",
    active: true,
    value: 45
}

class MySettings {
    settingList: Array<SettingItem> =[];

    constructor (public id: number, public status: boolean) {    }
    
    setId(id: number): void {this.id = id}
    getId(): number {return this.id;}
    
    getSetting(index: number): SettingItem{
        return this.settingList[index];
    }

    setSetting(item: SettingItem) {
        this.settingList.push(item);
    } 
}

let mySettings = new MySettings(1, true);
mySettings.setSetting(mySetting);
console.log(mySettings.settingList);

Когда создается определенный класс, ми можем указать, что класс должен имплементировать определенный интерфейс с помощью ключевого слова implements. Это наложит ограничение, что класс должен будет содержать у себя все поля, которые описанные в интерфейсе, если всех полей не будет — получим ошибку. Но это не запрещает добавлять в класс другие поля, описание которых нет в интерфейсе.
Также можно использовать интефейс для создания объекта, указав его в качестве типа объекта и как составной тип.
Следует отметить, что согласно рекомендациям команды Angular, более предпочтительно использовать классы вместо интерфейсов. Синтаксис использования в таком случае будет аналогичным интерфейсам. Эта рекомендация связана с тем, что все интерфейсы после транспиляции будут удалены в исходном JavaScript, а классы останутся и будут описывать модели данных.

Generics

Механизм который позволяет динамически менять тип с которым мы будем работать. Рассмотрим идею на примере простой функции.

function getStatus<T>(config: T): T {
    if (typeof config == 'boolean'){
        return config;
    }

    if (typeof config == 'string')
        return config;
}
let result1 = getStatus<boolean>(true);
console.log(result1);

let result2 = getStatus<string>("true");
console.log(result2);

Модули

Концепция модулей широко используется в Ангуляр и совпадает с концепцией в ES6.
Обычно под модулем понимается отдельный файл. Если необходимо сделать код, описанный в этом файле доступным извне, используют ключевое слово exports.
Соответственно, для того чтоб получить доступ к этому коду необходимо использовать ключевое слово import.
Рассмотрим несколько примеров.

import { Injectable } from '@angular/core';
import { Task, Urgency } from "./models/task.interface";


export class AppService {
    constructor() { }
}

и

import { Injectable } from '@angular/core';
import { Task, Urgency as Emergency} from "./models/task.interface";


class AppService {
    constructor() { }
}
//export { AppService }
export { AppService as AppServ }
  1. Вам необходимо реализовать конвертер весовых единиц. Для хранения типов весовых единиц используйте enum WeightType.
    Для хранения данных весовой единицы реализуйте класс class Weight содержащий два поля — значение веса и тип весовой единицы.
    Основной функционал должен быть реализован в class WeightConverter интерфейс которого содержит один метод convertTo(weight:Weight, type: WeightType):Weight, где weight — весовая единица которую необходимо преобразовать, type — тип в который необходимо конвертировать весовую единицу.
    Для реализации задачи воспользуйтесь данной заготовкой. Задачу реализовать в StackBlitz.
enum WeightType{
   gramm,
   kg,
   pound
}
class Weight{
   constructor(public weight: number, public type:WeightType) {}
}
class WeightConverter{
   convertTo(weight:Weight, type: WeightType): Weight {
       // тело функции может менятся. Return прописан для предотвращения ошибок.
       return new Weight(0, type);
   }
}
let convertor = new WeightConverter();
let weightInGramm = new Weight(1500, WeightType.gramm);
let weightInKg = convertor.convertTo(weightInGramm, WeightType.kg);
  1. Вам необходимо реализовать класс, который работает с очередью задач.
    Для этого создайте класс PriorityQueue<T>.
    Этот класс должен иметь два метода push(priority: number, t:T) — для добавления элемента в очередь (уже реализовано в заготовке) и pop(): T — возвращающий элемент с наивысшим приоритетом, удаляя его при этом его из очереди.
    В случае если несколько элементов имеют одинаковый приоритет, для простоты, метод должен вернуть первый найденный элемент в очереди.
    Для хранения самих элементов очереди необходимо реализовать класс QueueElement<T> имеющий два поля — приоритет и собственно сам элемент.
    Для реализации задачи воспользуйтесь данной заготовкой. Задачу реализовать в StackBlitz.
class QueueElement<T> {
   constructor(private priority: number, private t: T){}
}
 
class PriorityQueue<T> {
 
   list: Array<QueueElement<T>> = [];
 
   push(priority: number, t:T) {
       this.list.push(new QueueElement<T>(priority, t));
   }
 
   pop(): T {
       return null;
   }
}
 
let list = new PriorityQueue<string>();
list.push(1, "Element1")
list.push(14, "Element2")

Варианты решения задач

Задача #1

Задача #2

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

Задание
  1. Зарегистрироваться на облачном сервисе mlab.com . Под существующем профилем создать новую базу данных.
  2. Внести необходимые изменения в заготовку, для работы с созданной базой данных.
  3. Создать в заготовке новую модель для “Закладок” и подготовить необходимые маршруты для CRUD-операций.
  4. В существующем angular-приложении с предыдущих заданий, в карточках с фильмами создать две кнопки: “Добавить в избранное” и “Добавить в закладки”.
  5. Создать в angular-приложении новый сервис для работы с бэкендом для избранного.
    “Повесить” на созданные кнопки необходимые обработчики, которые будут вызывать через сервис запросы на необходимые CRUD-операции.

Заготовка для выполнения домашнего задания

DEPENDENCY INJECTION

Опишем ситуацию…

Очень часто в процессе написания приложения, необходимо непосредственно создавать объекты с которыми вы работаете.

Рассмотрим ситуацию на таком схематическом примере.

export class ScreenController {
    private model: Model;
    private view: View;
    
    constructor() {
        this.model = new Model();
        this.view = new View();
    }
    init() {}
}

class Model{
    //some code
}

class View{
    //some code
}

// create instance
let screenCtrl = new ScreenController();

В чем проблема?

На первый взгляд ничего не обычного. Но…

Такой класс очень сильно зависит от Model и от View.

  • — Если один из этих классов будет модифицирован и, например, будет принимать параметры (измениться конструктор), то придется модифицировать сам ScreenController.
  • — Если захотим поменять класс Model на UserModel (заменить модель) то придется модифицировать ScreenController.
  • — Если бы мы хотели использовать один и тот же экземпляр View в разных контроллерах, то у нас бы это не получилось.
export class ScreenController {
    private model: Model;
    private view: View;
    
    constructor() {
        this.model = new UserModel();
        this.view = new View({isVisible: true});
    }
    init() {}
}
let screenCtrl = new ScreenController();

Что делать?

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

export class ScreenController {

    constructor(private model: Model, private view: View) {}
    init() {}

}
let screenCtrl = new ScreenController(new Model(), new View());

let newScreenCtrl = new ScreenController(new UserModel(), new View({isVisible: true}));

В такой реализации ScreenController уже меньше зависит от Model и View.

Однако теперь хочется “оптимизировать” создания самого ScreenController.

Для этого используют паттерн “Фабрика”.

export class ScreenController {
    constructor(private model: Model, private view: View) {}
    init() {}

}
export class ScreenControllerFactory {
    
    createScreen() {
        return new ScreenController(createModel(), createView());
    }

    createModel() {
        return new Engine();
    }

    createView() {
        return new Tires();
    }
}

let factory = ScreenControllerFactory();
factory.createScreen();

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

Вот пример из фреймворка Backbone.

Для создания контроллера приходится создавать все экземпляры «вручную» с помощью ключевого слова new.

define(['Utils','RequestModel', 'UserModel','UserView', 'UserCollection'
], function() {

    var Controller = Backbone.Router.extend({
        start : function () {
            var view = new UserView({
                collection : new UserCollection({
                    model : UserModel
                }),
                requestModel : new RequestModel(),
                utils : Utils
            });
        }});
    new Controller().start();

});

К счастью в Angular встроен Dependency Injection Framework, благодаря которому все эти операции проходят автоматически. Без участия разработчика.

Dependency Injection

“Паттерн программирования, в котором класс получает зависимости из внешних источников, вместо того чтоб формировать все самостоятельно.
Именно благодаря этому работа с сервисами в Angular такая простая.

Необходимо соблюдать всего несколько правил по правильному их подключению, которые мы сейчас и рассмотрим.

Что такое сервиса в Angular?

В отличии от первой версии Angular существует всего одна версия сервисов — это обыкновенные TypeScript классы.
Сервиса используются для:

  • — взаимодействия с Backend;
  • — хранения данных;
  • — выполнения вычислений, преобразований и другой бизнес логики

Пример простейшего сервиса:

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  constructor() {
  }

  private _count = 0;

  get count() {
    return this._count;
  }

  increaseCount() {
    this._count++;
    return this.count;
  }
}

Механизм работы с сервисами:

  1. описываете сервис
  2. настраиваете его Provider одним из трех способов
  3. внедряете сервис в конструктор компонента
@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  
  count: number;
  constructor(private counter: CounterService) {  }

  ngOnInit() {
    this.count = this.counter.count;
  }

  increase() {
    this.counter.increaseCount();
    this.count = this.counter.count;
  }

}
Особенности работы механизма DI в Angular:
  • — в процессе запуска приложения Angular использует специальный механизм (Injector), который в дальнейшем занимается внедрением сервисов туда, где это необходимо
  • — конфигурация происходит путем прописывания так называемых провайдеров (Providers), которые непосредственно создают сервиса
  • — для внедрения сервиса Injector обратится к Provider, который создаст необходимый экземпляр сервиса или вернет уже существующий экземпляр
  • — механизм Injector иерархический, таким образом если:
    • — сервис “внедренный” на верхнем уровне (родительский компонент) будет использоваться всеми наследниками
    • — если у наследника (дочерний компонент) “внедрен” тот же сервис, то для него будет создан отдельный экземпляр.
Настройка Provider:
  1. указать ключ “providedIn” внутри декоратора @Injectable
  2. задать конфигурацию в поле “providers” в настройках модуля в декораторе @NgModule
  3. задать конфигурацию в поле “providers” в настройках компонента внутри декоратора @Component
@Injectable({
  providedIn: 'root'
})
export class CounterService {
  constructor() {  }
}
@NgModule({
  imports: [
    BrowserModule,
    HttpModule
  ],
  declarations: [
    UserComponent
  ],
  bootstrap: [ AppComponent ],
  providers: [
    CounterService 
  ]
})
export class AppModule { }
@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css'],
  providers: [CounterService]
})
export class CounterComponent implements OnInit {
  
  count: number;
  constructor(private counter: CounterService) {  }

  ngOnInit() {
    this.count = this.counter.count;
  }

  increase() {
    this.counter.increaseCount();
    this.count = this.counter.count;
  }
}

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

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  constructor() {  }
}
@Injectable({
  providedIn: UserModule
})
export class CounterService {
  constructor() {  }
}

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

ИЕРАРХИЯ СЕРВИСОВ

Следует помнить, что каждый компонент/модуль Angular приложения содержит свой инжектор.
Все инжекторы связанны в иерархическое дерево.

Когда вы внедряете зависимость в конструктор компонента происходят следующие процессы:

  • — инжектор смотрит есть ли провайдер на текущем уровне (то есть у текущего компонента)
  • — если провайдера нет, поиск происходит на один уровень выше
  • — поиск происходит поэтапно в верх, пока не дойдет до root уровня

ДЕТАЛЬНАЯ НАСТРОЙКА PROVIDER

Наиболее частый сценарий работы — это создание обычного класса и указание его в providers.

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css'],
  providers: [CounterService]
})
export class CounterComponent implements OnInit {
  
  count: number;
  constructor(private counter: CounterService) {  }

  ngOnInit() {
    this.count = this.counter.count;
  }

  increase() {
    this.counter.increaseCount();
    this.count = this.counter.count;
  }
}

Однако существуют альтернативные способы конфигурации сервисов. Рассмотрим их более детально.

ProvidedIn

При создании сервиса с помощью Angular-cli у него автоматически будет прописан декоратор Injectable с конфигурационным объектом.

Значение ключа providedIn указывает на каком уровне будет внедрятся сервис.

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

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  constructor() {
  }

  private _count = 0;

  get count() {
    return this._count;
  }

  increaseCount() {
    this._count++;
    return this.count;
  }
}
Provider Object Literal

Самая распространенная запись (1) по сути является сокращением записи (2), где

  • — provide это ключ или токен, под которым регистрируется сервис
  • — useClass это “provider definition object”. Указывает на класс, который непосредственно будет зарегистрирован под этим ключом
  • — потенциально вы можете указать другой класс, который будет предоставлять сервис (3)
@NgModule({
  declarations: [
    AppComponent,
    CounterComponent,
    OtherCounterComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule
  ],
  providers: [
    CounterService,                                          (1)
    {provide: LoggerService, useClass: LoggerService}        (2)
    //{provide: LoggerService, useClass: OtherLoggerService} (3)   
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}
Aliased class providers

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

Это может быть полезно если вы по какой то причине не можете заменить название сервиса в самом компоненте.

@NgModule({
  declarations: [
    AppComponent,
    CounterComponent,
    OtherCounterComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule
  ],
  providers: [
   CounterService,
    {provide: LoggerService, useClass: LoggerService},
    // в системе зарегистрируется OtherLoggerService 
    // но при вызове реально сработает LoggerService
    {provide: OtherLoggerService, useExisting: LoggerService},
    //{provide: OtherLoggerService, useClass: OtherLoggerService}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}
Value provider

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

Это можно сделать благодаря useValue . Здесь важно, что сервис с таким именем все таки должен существовать.

import {CounterService} from './counter.service';

const localCounter: CounterService = {
  count: 0,
  increaseCount: function () { }
};

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    // {provide: CounterService, useClass: CounterService},
    {provide: CounterService, useValue: localCounter}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}
Non-class dependencies

Теоретически вы можете инжектать не только класс но и объект или функцию.

Допустим у вас есть набор констант или конфигурационный объект, который хотелось бы выделить в отдельную структуру но при этом делать класс нет большого смысла.

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

Такой ключ-токен можно создать с использованием InjectionToken .

Создадим все необходимые нам структуры в отдельном файле, например counter.ts

import {InjectionToken} from '@angular/core';
import {CounterService} from './counter.service';

export const localCounter = {
  count: 0,
  increaseCount: function () {
    this.count += 2;
    console.log(`from Object - ${this.count}`);
    return this.count;
  }
};

export const LOCAL_COUNTER = new InjectionToken<CounterService>('qwerty');

Затем, подключим как обычно в модуле.

import {CounterService} from './counter.service';

const localCounter: CounterService = {
  count: 0,
  increaseCount: function () { }
};

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    CounterService,
    {provide: LOCAL_COUNTER, useValue: localCounter}
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

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

import {CounterService} from '../counter.service';
import {LOCAL_COUNTER} from '../counter';

export class CounterComponent implements OnInit {
  count: number;

  // CounterService - используется просто как тип
  constructor(@Inject(LOCAL_COUNTER) public counter: CounterService) {
  }

  ngOnInit() {    
  }

  increase() {    
  }

}
Factory provider

Этот подход позволяет создавать сервис “вручную” и передать параметры в его конструктор.

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

import {CounterService} from './counter.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    {
      provide: CounterService,
      useFactory: () => {
        return new CounterService({startValue: 777});
      }
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

В самом сервисе вы можете принять и обработать этот параметр.

export class CounterService {
  count = 0;

  constructor(private config: {startValue: number}) {
    this.count = this.config.startValue;
  }

  increaseCount() {
    this.count++;
    console.log(`from Class - ${this.count}`);
    return this.count;
  }
}

Кроме статических значений в такую Фабрику можно так же передать и другие зависимости.

import {CounterService} from './counter.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [UserService, {
    provide: CounterService,
    useFactory: (user: UserService) => {
        return new CounterService(user.isActive());
    },
    deps: [UserService]
  }],
  bootstrap: [AppComponent]
})

export class AppModule {
}
Optional dependencies

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

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  count: number;
  constructor(@Optional() private counter: CounterService) {
  }

  ngOnInit() {
    if (this.counter) {
      this.count = this.counter.count;
    } else {
      this.count = 100;
      console.log('Use the built-in counter');
    }
  }
}
Injecting injector

Следует знать о том, что вы можете в качестве зависимости внедрить сам инжектор.

Хотя этот подход не является рекомендуемым.

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent implements OnInit {
  count: number;
  counter: CounterService; // CounterService только для типа
  
  constructor(private injector: Injector) {
    this.counter = this.injector.get(CounterService);
  }

  ngOnInit() {
    this.count = this.counter.count;
  }

  increase() {
    this.counter.increaseCount();
    this.count = this.counter.count;
  }

}

Материалы лекции

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

Игра «Жизнь»

Необходимо реализовать небольшое angular-приложение, которое будет представлять собой браузерную версию игры «Жизнь», более подробно на вики.

Для реализации можете воспользоваться заготовкой.

Готовое приложение должно состоять из квадратного поля, на котором размещены клетки.
У клетки должны быть два состояния, быть «живой» (выделить одним цветом) или быть «мёртвой» (выделить другим). Клетка имеет восемь соседей, окружающих её.

Для управления приложением необходимо создать панель с кнопками. 1-я кнопка должна запускать процесс отрисовки, 2-я останавливать процесс и очищать поле (заполнять поле «мёртвыми» клетками), 3-я заполняет поле «живыми» или «мёртвыми» клетками в случайном порядке. Дополнительно на панели должен присутствовать слайдер, с помощью которого будет возможность управлять скоростью отрисовки, то есть самой игры. Минимальное значение скорости отрисовки 1 поколение клеток за секунду.

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

  • — В пустой (мёртвой) клетке, рядом с которой ровно три живые клетки, зарождается жизнь
  • — Если у живой клетки есть две или три живые соседки, то эта клетка продолжает жить; в противном случае, если соседей меньше двух или больше трёх, клетка умирает («от одиночества» или «от перенаселённости»).

Стоит обратить внимание на граничные условия. Приложение можно сделать в простом и более сложном вариантах:

  • — В более простом поле с клетками будет считаться замкнутым, то есть с границами поля сами клетки никак не взаимодействуют.
  • — В сложном верхняя граница поля «соединена» с нижней, а левая граница — с правой, что представляет собой эмуляцию поверхности тора (бублика), то есть самый нижний ряд клеток взаимодействует с самым верхним, а ряд клеток на правой границе с рядом на левой.

Игра “Крестики-нолики”

Реализуйте браузерную версию игры “Крестики-нолики” в виде небольшого angular-приложения. Если правила игры Вам неизвестны, можете ознакомиться на вики.

Необходимо реализовать поле с 9-тю клетками. При клике на клетку в зависимости от очередности хода, ставится знак “Х” или “0”.
Можно реализовать так, что бы “ход” мог делать только пользователь, имитируя таким образом игру между двумя игроками. Или реализовать “ход” противника программно в коде, имитируя таким образом игру с компьютером. Например, в первом “ходе” компьютер ставит свой знак в случайном порядке, а в последующих старается поставить его по “соседству” со знаком, поставленным пользователем в предыдущем ходе (или другим, более “хитрым алгоритмом”).
Игра заканчивается, согласно правилам, если кто-то из игроков выстроит в ряд 3 своих фигуры по вертикали, горизонтали или диагонали.

ЗАДАНИЕ

В этом задании, Вам необходимо будет продолжить работу над приложением о фильмах.

Задание:

  1. Создать отдельный компонент со своим роутом, который будет отвечать за поиск фильмов уже непосредственно используя api themoviedb. Как входной параметр принимать в него из search-component строку запроса. Через методы в сервисе получать результаты поиска и отображать, подобным образом как в компоненте с популярными фильмами. Сохранить при этом функционал по добавлению фильмов в “избранное”.
  2. Все используемые в приложении константы вынести в отдельный файл, например config.ts в виде объекта и внедрить его как зависимость в сервиса. Для реализации использовать методику Non-class dependencies, пример которой мы рассматривали на сессии.

ЗАДАЧИ:

  1. Необходимо реализовать калькулятор стоимости ремонта. А именно покраски стен.
    Все вычисление необходимо сделать в сервисе.Пользователь указывает следующие данные:
  • — длина комнаты в метрах
  • — ширина комнаты в метрах
  • — высоту комнаты в метрах
  • — расход краски в литрах на один квадратный метр.Значения, которые пользователь будет указывать в поле должны находиться в диапазоне 0,1-0,5 литра за квадратный метр
  • — цена краски за банку 5 литров
  • — количество слоев покраски.Ограничить поле диапазоном 1-4 раза.

В результате работы программы пользователю должно быть показано сообщение.
“Площадь ваших стен составляет [number] м. кв.
Для покраски вам необходимо [number] банок краски”
Внимание. результат должен быть не в литрах а в целых банках краски.
Задачу выполнить в StackBlitz/CodeSandbox.

  1. Конвертер валют.
    Вам необходимо реализовать форму, которая должна содержать 2 select box и 1 input.
    1 select box — из какой валюты конвертировать.
    2 select box — в какую валюту конвертировать.
    В конверторе должны использоваться три основные валюты: Гривны, Доллары, Евро.
    По желанию, можете добавить Биткоины.
    Коэффициенты для конвертации укажите в самом сервисе. В коде компонента вы должны снимать значения с формы и передавать их в сервис.
    Выполнять все расчеты в сервисе и с помощью специального метода (getConvertedValue) получать сконвертированую валюту и выводить ее на экран.
    Задачу выполнить в StackBlitz/CodeSandbox.