Открыть меню    

Angular 2+

Более подробно об Angular вы можете узнать на сайте dnzl.ru (Angular). На dnzl.ru присутствует целый раздел посвященный непосредственно Angular.

Концепция Angular

  • export - 'раскрывает' некоторые компоненты/директивы/фильтры другим модулям приложения. Без этого компоненты/директивы/фильтры определенные в модуле доступны лишь в этом модуле.
  • imports импортрируем сторонние модули (необходимые для работы), тем самым делая экспортированные объявления (exported declarations) других модулей доступными в текущем модуле, например, AutoCompleteModule из primeng
  • declarations - указываем компоненты, директивы и pipes, которые принадлежат модулю. Тем самым делаем эти директивы, включая компоненты и фильтры, из текущего модуля доступными для других директив в текущем модуле. Селекторы директив, компонентов или фильтров сопостовляются только с HTML, если они объявлены или импортированы (declared or imported)
  • providers - делают сервисы и значения доступными для DI. Они добавляются в корневой scope и они вводятся (injected) в другие сервисы или директивы, которые имеют их как зависимости.
  • bootstrap - здесь мы указываем то, что нужно запустить при старте приложения.

stackoverflow.com: В чем разница между declarations, providers и import в NgModule

@angular/core

Возможности import рассмотрим на примере:

import {
    Component,
    Pipe,
    Directive,
    NgModule,
    Input,
    Output,
    ViewEncapsulation,
    EventEmitter,
    PipeTransform,
    OnInit,
    HostListener
} from '@angular/core';

Модули

Angular 2 состоит из модулей. Приложение на angular 2 также является модулем.

Мы можем создавать свои модули для объединения сущностей по типу. В будущем этом поспособствует скорости загрузки приложения, например, с помощью Lazy Loading.

Все модули подключаются в массив imports:

 @NgModule({
    imports: [
        PhrasesModule,
        //...

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

Вот собственно говоря пример стороннего модуля (связка по типу фразы/Phrase):

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";

import { PhraseListComponent } from "./phrase-list/phrase-list.component";
import { PhraseDetailsComponent } from "./phrase-details/phrase-details.component";

import { PhrasesRoutingModule } from "./phrases-routing.module";
import { PhraseService } from "./shared/phrase.service";

@NgModule({
    imports: [
        // CommonModule - предоставляет базовые возможности ангуляра (ngIf etc.)
        CommonModule,
        // настройки маршрутизации для модуля PhrasesModule
        // если вы перенесете данный модуль в app.module.ts, то работать не будет
        PhrasesRoutingModule
    ],
    exports: [
        // этим мы далаем данный компонент доступным в компонентах родит-го модуля
        PhraseListComponent
    ],
    declarations: [
        PhraseDetailsComponent,
        PhraseListComponent
    ],
    providers: [
        PhraseService
    ]
})
export class PhrasesModule { }

     Модуль для роутинга (пример)

//app/app-routing.module.ts
import { NgModule } from '@angular/core';

import { RouterModule } from '@angular/router';
import {WelcomeComponent} from "./home/welcome.component";
import {PageNotFoundComponent} from "./page-not-found.component";

@NgModule({
    imports: [
        //II
        RouterModule.forRoot([{
            path: 'welcome',
            component: WelcomeComponent
        }, {
            path: '',
            redirectTo: 'welcome',
            pathMatch: 'full'
        }, {
            path: '**',
            component: PageNotFoundComponent
        }
        ]),
    ],
    exports: [
        // делаем так, чтобы app.module.ts мог его использовать
        RouterModule
    ]
})
export class AppRoutingModule { }
// подключаем в основном модуле (app/app.module.ts)
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
      AppRoutingModule,
  ],

     Регистрируем директиву (etc.) для всего приложения

Допустим, у нас есть директива, но задекларированная в общем модуле, если мы добавим ее (посредством declaration) в дочерний модуль, то js выдаст ошибку с примерно таким описанием: нельзя задекларировать директиву дважды. Отсюда возникает вопрос - как обойти эту проблему?

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

LAZY LOADING

Декомпозиция модулей (разбиение на модули) нужна в том числе для Lazy Loading (ленивая загрузка) - данную особенность предоставляют модули. Обычно приложение загружает весь js со всеми модулями, но зачем нам загружать все страницы, к примеру, если тот же юзер на второстепенные страницы может не перейти? Незачем. Поэтому мы можем вынести второстепенные страницы в отдельные модули и загружать их по необходимости.

Вот и рассмотрим на примере роутинга, то есть подключение модуелей с Lazy Loading по мере необходимости. Как видите мы подключаем модуль указав путь, а не через import, ибо import загрузит модуль сразу, а нам это не надо, также удалите import { BooksModule } в app.module.ts. Мы не будем использовать import, чтобы не webpack сразу не подключил наш файл в приложение.

loadChildren - значением данного свойства является строка, чтобы webpack ничего заранее не загрузил.

// app-routing.module.ts

const appRoutes: Routes = [
  { path: '', component: MainPageComponent },
  // BooksModule - имя модуля
  { path: 'books', loadChildren: './books-page/books.module#BooksModule' }
];

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

// не забудьте удалить import { BooksModule } в app.module.ts

Теперь при переходе на страницу books в консоли вы сможете увидеть подгружаемый chank.

Отметьте, что для Guard вместо canActivate: [AuthGuardAdmin] нужно писать canLoad: [AuthGuardAdmin]

Компоненты

Компоненты отвечают за внешний вид и за взаимодействие с пользователем.

Компоненты представляют основные строительные блоки приложения Angular 2. Каждое приложение Angular имеет как минимум один компонент.

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

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

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

  • слова разделяться дефисом
  • желательно не использовать заглавные буквы
  • обязателен закрывающий тег
  • <custom-comp></custom-comp>

Директивы

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

     Структурная директива

Структурная директива (на это указывает *), которая меняет DOM - (например, ngFor - добавляет элементы)

  • *ngIf - удаляет или создает часть дерева DOM на основании выражения.
  • *ngFor
<todo-item *ngFor="let todo of todos" 

     Атрибутные директивы

Атрибутные директивы (на это указывает []) предназначены для изменения внешнего вида или поведения элементов (они не создают и не удаляют элементы, например, [ngClass])

<div class="todo-item"  [ngClass]=" { 'completed' : todo.completed } ">
  • [ngStyle]
  • [ngClass]
  • [ngSwitch]
  • [ngSwitchWhen]
  • [ngSwitchDefault]

     ngSwitch

ngSwitch - директива используется для того, чтобы отобразить один элемент (включая дочерние) из набора возможных элементов. При использовании директив ngSwitchCase и ngSwitchDefault обязательно нужно использовать (*). Можно использовать любой тип данных при установке значения в ngSwitchCase

<div [ngSwitch]="choice">
    <div *ngSwitchCase="'1'">
        Выбор 1
    </div>
    <div *ngSwitchCase="'2'">
        Выбор 2
    </div>
    <div *ngSwitchCase="'3'">
        Выбор 3
    </div>
    <div *ngSwitchDefault>
        Выбор по умолчанию
    </div>
</div>

Сделайте свой Выбор <input type="text" [(ngModel)]="choice" />

     Свойство selector

Свойство selector позволяет определить к какому DOM-элементу или компоненту применить директиву. Свойство selector:

// Custom Directives
@Directive({
    // нам нужно придать поведение, например, компоненту с атрибутом [task]
    selector: '[task]'
})
class TaskMyDirective {
    private defaultMyText: string;
    //...

     Пользовательские директивы

Импортируем класс Directive

import {
    Directive,

} from '@angular/core';

@HostListener - с помощью данного декоратора можно подключить обработчики событий, возникающие во вмещающем компоненте.

@HostListener('mouseover')
onMouseOver() {
    //toDO
}

     Выходные свойства и директивы

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

<div *ngFor="let item of cars"
        [my-attr] = "'custom-class'"
        [my-car]="item" (my-category)="setCat($event)">   
@Directive({
    selector: "[my-attr]"
})
export class MyAttrDirective {

    @Input("my-attr")
    @HostBinding("class")
    bgClass: string;

    @Input("my-car")
    car: Car;

    @Output("my-category")
    click = new EventEmitter<string>();

    @HostListener("click")
    triggerCustomEvent() {
        this.click.emit(this.car.category);
    }
}

При это, как видите, в классе директивы my-attr мы работаем со множеством входных свойств, которые необходимы для реализации логики директивы.

     Кастомная директива: renderer, @HostBinding, передача параметров

<div appCustomTest="" [hoverColor] ="'red'">
    <b>тестируем директиву</b>
</div>
import { Directive, ElementRef, OnInit, Renderer2, HostBinding, Input } from '@angular/core';
import { HostListener } from "@angular/core";

// указываем декоратор, который укажет, что это Директива
@Directive({
    // директивы используются, как правило, как атрибуты
    selector: '[appCustomTest]'
})
export class CustomTestDirective implements OnInit{


    @HostBinding('style.border') border: string;

    @Input() hoverColor: string = 'green';


    // в данный класс нужно сделать Инъекцию элемента,
    // то есть тот элемент, котор будет приходить в директиву
    constructor(private el: ElementRef,
                private renderer: Renderer2) {}

    ngOnInit() {

        /*
        console.log('this.el: ', this.el);
        // this.el.nativeElement - является обычным Dom узлом
        this.el.nativeElement.style.color = 'white';
        this.el.nativeElement.style.backgroundColor = 'black';

        // используем renderer (setStyle):
        this.renderer.setStyle(this.el.nativeElement, 'background-color', 'black')
        this.renderer.setStyle(this.el.nativeElement, 'color', 'white')


        this.renderer.addClass(this.el.nativeElement, 'test__white-text__bg-black');
         */

    }

    @HostListener('mouseenter', ['$event']) mouseEnter(event: Event) {
        //console.log('event: ', event);
        this.renderer.addClass(this.el.nativeElement, 'test__white-text__bg-black');
        this.border = `2px solid ${this.hoverColor}`;
    }


    @HostListener('mouseleave', ['$event']) mouseLeave(event: Event) {
        this.renderer.removeClass(this.el.nativeElement, 'test__white-text__bg-black');
        this.border = '2px solid brown';
    }
}

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

this.el.nativeElement.style.backgroundColor = 'black';

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

Поэтому в angular есть специальный интерфейс, котор. называется renderer (см. пример выше)

     @HostBinding (привязка свойств)

Декоратор @HostBinding позволяет привязать, например, какое либо свойство (например, border). Данная связка будет положена в переменную, см. пример выше, через которую мы сможем менять это свойство.

     @HostListener (привязка событий)

см. пример выше для события mouseenter.

     Передача параметров

Параметр в директиву можно передать через обычное входящее свойство: [hoverColor] = "'red'" (см. пример выше). Или как значение непосредственно самой директивы.

     Создание двусторонней привязки для директивы

Да, директивы поддерживают пользовательскую привязку [()]. Функциональность двусторонней привязки опирается на соглашение об именах.

Привязка осуществляется на базе входного свойства и метода ngOnChanges - это из модели в директиву. И через слушателя события input.

<div class="form-group">
    <label>Имя:</label>
    <input class="form-control" [(myModel)]="newProduct.name" #myModel="myModel" />
    <div>Направление: {{myModel.direction}}</div>
</div>


<form [formGroup]="form" >
    <label>Имя</label>
    <input class="form-control"
           name="name"
           [(ngModel)]="newProduct['name']"
           formControlName="name" />
</form>
@Directive({
    selector: "input[myModel]",
    exportAs: "myModel"
})
export class myModel {

    direction: string = "None";

    @Input("myModel")
    modelProperty: string;

    @HostBinding("value")
    fieldValue: string = "";


    // от модели директиве
    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        let change = changes["modelProperty"];
        if (change.currentValue != this.fieldValue) {
            this.fieldValue = changes["modelProperty"].currentValue || "";
            this.direction = "Model";
        }
    }

    @Output("myModelChange")
    update = new EventEmitter<string>();


    // от директивы модели
    @HostListener("input", ["$event.target.value"])
    updateValue(newValue: string) {
        this.fieldValue = newValue;
        this.update.emit(newValue);
        this.direction = "Element";
    }
}

     Директива ngSwitch

Директива ngSwitch появилась в 4-м angular. Директиву ngSwitch удобно использовать, например, для табов.

<ul class="list-group">
    <li *ngFor="let item of items" (click)="onclick(item)"  class="list-group-item">
        {{ item }}
    </li>
</ul>

<div [ngSwitch] ="current">
    <p *ngSwitchCase="1">
        1
    </p>
    <p *ngSwitchCase="2">
        2
    </p>
    <p *ngSwitchCase="3">
        3
    </p>
    <p *ngSwitchCase="4">
        4
    </p>
    <p *ngSwitchDefault>
        default
    </p>
</div>
// for ngSwitch
items = [1, 2, 3, 4, 5];
current = 1;

onclick(number: number) {
    this.current = number;
}

Фильтры (pipes)

Фильтры - так называемые 'помощники', которые форматируют вывод данных в шаблонах в зависимости от наших потребностей.

<h1> {{ minutes }}:{{ seconds | number: '2.0' }} </h1>
  • uppercase
  • lowercase
  • number
  • percent
  • currency
  • slice
  • date
  • json
  • replace
  • i18nPlural - число по объекту в строку
  • i18nSelect - анализирует строку
  • async - наблюдает за асинхронными данными

     Пользовательские фильтры

Импортируем декоратор pipe и интерфейс PipeTransform

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

Реализуем класс:

@Pipe({
    name: 'nameFilter',
    pure: false // если false, то фильтр будет следить за изменением состояния
})

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

  • 1 - входные данные
  • 2 - параметры для настройки фильтра

И возвращает преобразованные данные

@Pipe({
    name: 'componentFormattedTime'
})
class FormattedTimePipe implements PipeTransform {
    transform(totalMinutes: number): string {
        let minutes: number = totalMinutes % 60;
        let hours: number = Math.floor(totalMinutes / 60);
        return `${hours}h:${minutes}m`;
    }
}

В шаблоне:

<p>{{ 90 | componentFormattedTime }}</p>
@NgModule({
    imports: [BrowserModule],
    declarations: [
        //...
        FormattedTimePipe,
        //...
    ],
    bootstrap: [MyComponent],
})

Декоратор

Декоратор - особенность, появившаяся в ES7, и затем добавленная в TypeScript, позволяющая добавлять метаданные в классы.

Все декораторы узнаются по префиксу @

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

function Greeter(target: Function): void {
  target.prototype.greet = function(): void {
    console.log('Привет!');
  }
}

@Greeter
class Greeting {
  constructor() {}
}

var myGreeting = new Greeting();
myGreeting.greet();   //  'Привет!'

     Декоратор @ViewChild

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

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

// child
@Component({
    selector: 'app-block',
    templateUrl: './block.component.html',
    styleUrls: ['./block.component.scss']
})
export class BlockComponent implements OnInit {
    private visible: boolean = false;

    constructor() {}
    ngOnInit() {}

    show() {
        this.visible =  true;
    }
// child шаблон
<div *ngIf="visible">
    visible block
</div>

host компонент

// parent component
export class TestComponent implements OnInit {

    // для получения доступа к дочернему компоненту
    @ViewChild(BlockComponent)
    block: BlockComponent;

    constructor() {}
    ngOnInit() {}

    showBlock() {
        this.block.show();
    }
// parent шаблон
<app-block></app-block>
<button (click) = "showBlock()" >button</button>

     Декоратор @ViewChildren

Декоратор @ViewChildren позволяет получить доступ сразу ко всем компонентам. В примере ниже получаем все дочерние компонеты (реализованы см. выше) в родительском компоненте.

// parent component
export class TestComponent implements OnInit {

    // для получения доступа к дочернему компоненту
    @ViewChild(BlockComponent)
    block: BlockComponent;

    // для получения доступа ко всем компонентам указанного типа
    @ViewChildren(BlockComponent)
    blocks: QueryList<BlockComponent>;

    showAll() {
        this.blocks.forEach(x => x.show());
    }
// parent шаблон
<app-block></app-block>
<app-block></app-block>
<app-block></app-block>
<button (click) = "showAll()" >showAll</button>

Чтобы получить из множества дочерних компонентов какой-то определенный можно задать template reference variable ( #heroInput ).

<app-block></app-block>
<app-block #my></app-block>
    @ViewChild('my')
    blockMySpecific: BlockComponent;

     Декоратор @ViewContent

@ViewChildren и @ViewChild работают лишь с теми компонентами, которые нахлдятся непосредственно в шаблоне. Если же мы получили компонент через ng-content то, чтобы получить к нему доступ нужно воспользоваться декоратором @ViewContent.

Template Reference Variable

Выше мы уже затронули template reference variable. Давайте расмотрим это понятие более подробно.

Template Reference Variable - ссылка на DOM элемент или директиву в шаблоне. К переменной можно обратиться в любой части шаблона, в котором она объявлена.

Есть два способа объявления переменной:

  • префикс #, например #varNameThere
  • префикс ref-, например, ref-varNameThere
a <input #aInput type="text"/> <br />
b <input ref-bInput type="text"/> <br />
<!--aInput - ссылка на элемент input, созданный в начале шаблона -->
<button (click)="calculateSum(aInput.value, bInput.value)">Рассчитать</button>

Result = {{result}}

ng-content

Рассмотрим использование ng-content на примере.

Шаблон компонента потомока:

<div *ngIf="visible">
    <h2>Message:</h2>
    <p>
        <ng-content></ng-content>
    </p>
    <button (click)="hide()">Hide</button>
</div>

Вмещающий компонент родитель:

<message-box>
    This is first message
</message-box>
<message-box>
    Lorem ipsum dolor sit amet....
</message-box>

Типы привязки

Типы привязки могут быть сгруппированы в три категории в зависимости от потока данных: от source-to-view, от view-to-source, и в двунаправленной последовательности: view-to-source-to-view:

Направление данных Синтаксис Тип
Однонаправленный от источника данных к view
{{expression}}
                [target]="expression"
                bind-target="expression"
Interpolation
Property
Attribute
Class
Style
Однонаправленный от view к источнику с данными
(target)="statement"
                on-target="statement"
Event
Двунаправленный
[(target)]="expression"
                bindon-target="expression"
Two-way

Атрибуты

     [] Входное свойство

[] - сообщают феймворку, что атрибут является входным свойством.

Если требуется связать свойство с переменной компонента, то: [my_prop]="{{valueProp}}".

<countdown
    [seconds]="25">
</countdown>
class CountdownComponent {
    @Input() seconds: number;
    //...

Декортатор Input

Декортатор Input указывают, какие свойства вы можете установить на компоненте.

В самом todo-item импортируем декортатор Input (им помечаются входные данные) -

<todo-item *ngFor="let todo of todos" [todo]="todo" 
 { Component, Input } from '@angular/core';

Используем в компоненте:

export class TodoItemComponent {
    //Декоратором @Input помечаются входные данные
    @Input() todo: Todo;

}

При удалении нужно передать данные от потомка родителю ((delete)="delete($event)"), один из способов - это события (можно подписаться на события пользовательских компонентов).

<div class="todo-list" *ngIf="todos.length > 0">
    <todo-item *ngFor="let todo of todos" [todo]="todo" (delete)="delete"></todo-item>
    <!--
        (delete) привязываемся к событию;
        ="delete" - метод у родительскокого компонента,
        который будет вызван по emit delete на todo-item
    -->
</div>

     () Выходное свойство

() - сообщают феймворку, что атрибут является выходным свойством, запускающее подключенный к нему обработчик событий.

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

<--todo-list.component.html-->

<todo-item
            [(size)]="sizeInToDo"
            *ngFor="let todo of todos"
            [todo]="todo"
            (delete)="delete($event)"
            (toggle)="toggle($event)"></todo-item>

     Двунаправленное связывание ( [(...)] )

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

Элемент принимает комбинацию из настройки специфического свойства и прослушивает события элемента.

Для этих целей у ангуляра есть специальное двунаправленное связывание: [(x)].

У свойства [(x)] будет соответствующее событие xChange.

Двунаправленное связывание (Two-way binding ( [(...)] ))

//todo-list.components.ts (родитель)

sizeInToDo: number;

ngOnInit() {
    this.sizeInToDo = 0;
}
<--todo-list.component.html-->

<todo-item [(size)]="sizeInToDo"></todo-item>

<div>
    size in todo-list: {{sizeInToDo}}
</div>
//todo-item.components.ts (потомок)
@Input() size: number | string;
@Output() sizeChange = new EventEmitter();

dec() {
    this.resize(-1);
}
inc() {
    this.resize(+1);
}

resize(delta: number) {
    this.size = +this.size + delta;
    this.sizeChange.emit(this.size);
}
<button (click) = "dec()">dec(-)</button>
<button (click) = "inc()">inc(+)</button>

Декоратор Output (EventEmitter)

В дочернем импортируем декоратор Output (им помечаются выходные данные)

Или Output идентифицируют события, которые компонент может инициировать для отправки информации по иерархии родительскому элементу.

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


import { Component, Input, Output, EventEmitter } from '@angular/core';

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

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

От ребенка (todo-item.component) родителю (todo-list.component.html)

<!-- todo-item.component.html -->
<div class="todo-item"  [ngClass]=" { 'completed' : todo.completed } ">
    <button class="checkbox icon" (click)="onToggle()">
export class TodoItemComponent {
    @Output() toggle = new EventEmitter();

    onToggle() {
        this.toggle.emit(this.todo);
    }
<!-- todo-list.component.html -->
<div class="todo-list" *ngIf="todos.length --> 0"-->
    <todo-item *ngFor="let todo of todos" [todo]="todo" (toggle)="toggle($event)"--></todo-item-->
export class TodoListComponent implements OnInit {

    toggle(todo: Todo) {
        this.todoService.toggleTodo(todo);
    }

Привязка данных: Шаблонные теги

Шаблонные теги (интерполяция) - один из видов привязки данных

Пример:

<!-- Шаблонные теги (интерполяция) - один из видов привязки данных -->
<i>
    {{ todo.completed ? 'check_box' : 'check_box_outline_blank' }}
</i>

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

Пример:

<i class="material-icons" [innerText] = "todo.completed ? 'check_box' : 'check_box_outline_blank'" >
</i>

Привязка события (event binding)

<button class="checkbox icon" (click)="toggle()">
    Hello
</button>

Формы

Для работы с формами используется модуль:

import { FormsModule } from '@angular/forms'; // импортируем модуль для работы с формами

Благодаря модулю FormsModule мы можем использовать ngModel для привязки к свойству, ngSubmit:

<!-- #todoForm="ngForm" : этим angular поместит в переменную todoForm объект формы  -->
<form class="todo-form" (ngSubmit)="create(); todoForm.reset()" #todoForm="ngForm">
    <input   name="title"
                [(ngModel)]="newTodoTitle"
                type="text"
                placeholder="Что нужно сделать?"
                required >
    <button type="submit" [disabled]="todoForm.form.invalid">
        Добавить
    </button>
</form>
export class AppComponent {
    newTodoTitle: string = '';
    // с использования модуля от angular FormsModule
    // (привязка по ngModel), а также использование (ngSubmit)
    create() {
        let todo: Todo = new Todo(this.newTodoTitle);
        this.todos.push(todo);
        this.newTodoTitle = '';
    }
}

     Директива NgForm

Директива NgForm - отслеживает состояние всех элементов формы.

Как отмечалось выше, конструкция #todoForm="ngForm" поместит в переменную todoForm объект формы. Но в качестве альтернативы вы можете воспользоваться декоратором @ViewChild, чтобы получить ссылку на форму непосредственно в компоненте:

@ViewChild('todoForm') form: NgForm;

     Директива NgModelGroup в TD подходе

Создает и связывает FormGroup экземпляр с DOM элементом. Иначе говоря позволяет объединять контролы в группу в TD (template driven) подходе.

    <div ngModelGroup="user" #user="ngModelGroup">
        <!-- тут лежат контролы -->
    </div>

К этому блоку можно также задать local reference #user="ngModelGroup" и проверять валидацию (выводить сообщения) как у обычного контрола.

     Директива NgModel

Директива NgModel создает экземпляр FormControl из модели и связывает его с элементом формы.

В формах оставлена двусторонняя привяка при помощи директивы NgModel.

[(ngModel)] - указывает на строковое свойство компонента, обеспечит обновление модели в обоих направлениях (иначе говоря, двусторонняя привязка данных).

    {{title}}
    <input name="title"
           [(ngModel)]="title"
           type="text"
           required />

Директиву ngModel можно использовать и без двусторонней привязки:


    <input name="title"
            ngModel
            #title = "ngModel"
            type="text"
            required />
<p *ngIf="title.invalid && title.touched" >Заполните поле</p>
            

Где #title = "ngModel" это reference на angular control.

     Валидация и NgModel

NgModel

<input [(ngModel)]="name" #ctrl="ngModel" required>

<p>Value: {{ name }}</p>
<p>Valid: {{ ctrl.valid }}</p>

<p *ngIf = "ctrl.valid" > Valid ok  </p>

     Валидация при помощи valueChanges и statusChanges

Более реактивную валидацию можно реализовать при помощи valueChanges и statusChanges.

Они оба возвращают Observable:

//loginForm: ControlGroup
const email = this.loginForm.controls['email'];
email.valueChanges.subscribe(value => {
    this.emailValid = (email.dirty && value.indexOf('@') < 0);
});

valueChanges

     Объект FormControl

FormControl

FormControl один из трех фундоментальных строительных блоков форма Angular, помимо FormGroup и FormArray.

Чтобы сообщить Angular о том, что input связан с name FormControl в классе (reactive-forms#create-the-template), вам потребуется в шаблоне воспользоваться директивой formControl, в angular 2 - ngControl.

export class HeroDetailComponent1 {
    name = new FormControl();
}
<input class="form-control" [formControl]="name">
  • Первый аргумент - значение
  • Второй аргумент - функция-валидатор
const ctrl = new FormControl('', Validators.required);
console.log(ctrl.value);     // ''
console.log(ctrl.status);   // 'INVALID'

     FormGroup

FormGroup

     FormBuilder

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

FormBuilder

<form [formGroup]="heroForm" novalidate>

Как видите ниже FormGroupDirectiver связывает существующую FormGroup с DOM элементом (в Angular 2 использовалась ngForModel).

import { Component }  from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

import { states } from './data-model';

export class HeroDetailComponent4 {
  heroForm: FormGroup;
  states = states;

  constructor(private fb: FormBuilder) {
    this.createForm();
  }

  createForm() {
    this.heroForm = this.fb.group({
      name: ['', Validators.required ],
      street: '',
      city: '',
      state: '',
      zip: '',
      power: '',
      sidekick: ''
    });
  }
}

Пользовательская валидация

     Императивный или декларативный подход

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

Есть принципиальная разница этих двух подходов.

Декларативная программа определяет, ЧТО должно быть осуществлено, в то время как императивная предписывает, КАК это будет осуществляться. Императвная программа использует язык команд для непосредственного управления ходом программы, а декларативная программа использует язык ОПИСАНИЙ для того, чтобы исполнитель САМ по этим описаниям создал алгоритм для достижения поставленной цели.

Например язык HTML является чисто декларативным языком, так как чисто описывает то, что потом осуществит браузер. При этом нам совершенно не интересно КАК он это осуществит. В императивных языках мы сами командуем тем КАК всё это осуществить.

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

Для angular характерны два подхода при создании форм:

  • Template driven - декларативный подход
  • Reactive way - императивный подход

     Реактивный подход и формы

1. В app.module.ts необходимо подключить ReactiveFormsModule

2. Определяем форму в компоненте:

  ngOnInit() {
    this.form = new FormGroup({
      email: new FormControl(''),
      pass: new FormControl(''),
            // 1-й параметр - значение по умолчанию
            // 2-й - валидаторы
      name: new FormControl('Vasya'),
    });
  }

3. Связываем форму с шаблоном:

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label>Email</label>
    <input type="text" class="form-control" formControlName="email">
  </div>
  <div class="form-group">
    <label>Пароль</label>
    <input type="password" class="form-control" formControlName="pass">
  </div>

formControlName - позволяет связать контрол в шаблоне с ключом в конструкторе FormGroup

     Группировка форм при реактивном подходе

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

    <div formGroupName="user">
        <!-- контролы -->

        <p *ngIf="form.get('user.email').invalid && form.get('user.email').touched">
            Введите корректный email
        </p>

    </div>
  // компонент
  ngOnInit() {
    this.form = new FormGroup({
      user: new FormGroup({
        email: new FormControl('', [Validators.required, Validators.email]),
        pass: new FormControl('', Validators.required),
      }),
      name: new FormControl('Vasya'),

     Валидация при реактивном подходе

    // компонент
  ngOnInit() {
    this.form = new FormGroup({
      email: new FormControl('', [Validators.required, Validators.email]),
<!-- шаблон -->
<p *ngIf="form.get('email').invalid && form.get('email').touched">Введите корректный email</p>

     Создаем свою валидацию

На сайте angular Custom validators прекрасно показано создание пользовательской валидации как для реактивного, так и для шаблонного подхода.

Обратите внимание, что для шаблонного подхода используется обертка ForbiddenValidatorDirective над непосредственно валидатором forbiddenNameValidator. Это нужно для того, чтобы мы могли использовать валидатор как атрибут. Уже отсюда видны преимущества реактивного подхода (в нем никакие обертки не нужны).

Декомпозиция

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

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

<todo-item *ngFor="let todo of todos" [todo]="todo"></todo-item>

Свойство todo будет доступно в компоненте todo-item

Класс EventEmitter

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

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

//authentication.service.ts
    userIslogin: EventEmitter<boolean>;
    constructor() {
        this.userIslogin = new EventEmitter();
    }

    login({ username, password }): Promise<boolean> {
        return new Promise(resolve => {
            let validCredentials: boolean = true;

            // toDo check User credentials

            // испускаем событие
            this.userIsloggedIn.emit(validCredentials);
            resolve(validCredentials);
        });
    }

Далее (не забудьте внедрить сервис аутенфикации в нужный компонент) мы уже в компоненте подписываемся (subscribe) на уведомления о входе и выходе пользователя из системы.

userIslogin: boolean;

    constructor(private authenticationService: AuthenticationService) {

        authenticationService.userIslogin.subscribe(userIslogin => {
            this.userIslogin = userIslogin;
        });
    }

Жизненный цикл компонента

Для решения задач связанных с жизненным циклом компонента angular предоставляет несколько методов: life cycle hooks (крюки жизненного цикла).

Например, ngOnInit (потребуется для сервиса) , интерфейс который называется OnInit и находится в @angular/core

     ngOnChanges

// { [propertyName: string]: SimpleChange } - объект с строковыми ключами и значением типа SimpleChange
ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
    let msgArray: string[] = []
    console.log('changes: ', changes);
    for (let propName in changes) {
        msgArray.push(`property = ${propName}, current value = ${changes[propName].currentValue}, previous value = ${changes[propName].previousValue}`)
    }
    console.log(msgArray.join("; "));
}

// поменяли свойство name компонента:
/*
changes:  Object { name: Object }  sample3.component.ts:18:8
property = name, current value = value 1510097941159, previous value = undefined
*/

Сервисы

Сервисы отвечают за получение, предоставление и работу с данными.

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

Проблема: логика по работе с задачами разбросана по компонентам:
(за добавление отвечает компонент app,
удаление - todo-list,
выполнение - todo-item).

Одним из вариантов разделения ответственности между различными частями приложения являются сервисы.

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

В нашем случае сервис (todo.service.js) будет отвечать за предоставление задач, их добавление и удаление.

Сервисы в angular 2 это простые классы.

Важно: клиенты, которые используют сервис, не знают и не должны знать как сервис получает данные.

     Сервисы: иерархия

Если сервис определен посредством декоратора @Component у компонента, то данный компонент будет использовать свой экземпляр сервиса, например:

@Component({
    selector: "component",
    templateUrl: "component.html",
    providers: [CounterService] // данный компонент использует свой экземпляр сервиса
})
export class Component {
    private counter: number = 0;
    constructor(private counterService: CounterService) { }

    //..

Иначе компонет будет использовать экземпляр сервсиа CounterService, который создан для текущего модуля:

// component.ts
// Данный компонент использует экземпляр сервсиа CounterService, который создан для текущего модуля.
@Component({
    selector: "component",
    templateUrl: "component.html"
})
export class Component {
    private counter: number = 0;
    constructor(private counterService: CounterService) { }
    //..


// main.module.ts
@NgModule({
    imports: [CommonModule],
    declarations: [Component],
    providers: [CounterService]
})
export class ServiceHierarchyModule { }

     Регистрация провайдера с использованием Provider Object Literal

@Component({ selector: "sample1", templateUrl: "sample1.component.html", providers: [{ provide: Logger, useClass: Logger }] // [{ provide: Logger, useClass: Logger }] - регистрация провайдера с использованием Provider Object Literal // первый параметр - token // второй параметр - provider definition object // альтернативный класс провайдера //providers: [{ provide: Logger, useClass: Logger2 }] // убрать комментарий }) export class Sample1Component { private message: string; constructor(private logger: Logger) { } logMessage() { this.logger.log(this.message); } }

     Внедрение зависимостей (dependencies injection)

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

В angular 2 имеется встроенный механизм внедрения зависимостей.

Внедрить сервис в компонент:

1. Компоненту добавить constructor и указать параметр с типом сервиса

2. Зарегистрировать сервис в приложении (файл app.module.ts):

//app.module.ts
import { TodoService } from './shared/todo.service.ts';

@NgModule({
    // ...
    // в свойстве providers мы регистрируем сервисы
    providers: [TodoService],

    // ...

})

     Пример сервиса

//todo.service.ts
import { todos } from './data';
import { Todo } from './todo';

export class TodoService {
    todos: Todo[] = todos;

    // возвращает задачи
    getTodos(): Todo[] {
        return this.todos;
    }

    createTodo(title: string) {
        let todo = new Todo(title);
        this.todos.push(todo);
    }

    deleteTodo(todo: Todo) {
        let index = this.todos.indexOf(todo);

        if (index > -1 ) {
            this.todos.splice(index, 1);
        }
    }

    toggleTodo(todo: Todo) {
        todo.completed = !todo.completed;
    }

}
<--todo-list.component.html-->

<div class="todo-list" *ngIf="todos.length > 0">
        <todo-item
            *ngFor="let todo of todos"
            [todo]="todo"
            (delete)="delete($event)"
            (toggle)="toggle($event)"></todo-item>
</div>
//todo-list.components.ts
// с использованием сервиса
import { Component, Input, OnInit } from '@angular/core';
import { Todo } from '../shared/todo';
//импортируем сервис
import { TodoService } from '../shared/todo.service';

@Component({
    moduleId: module.id,
    selector: 'todo-list',
    templateUrl: 'todo-list.component.html',
    styleUrls: ['todo-list.component.css']
})

export class TodoListComponent implements OnInit {
    todos: Todo[];
    todoService: TodoService;

    // конструктор предназначен для инициализации свойств
    // (если конструктор сработал это еще не значит, что компонент создан и готов к работе)
    constructor(todoService: TodoService) {
        this.todoService = todoService;
        this.todos = [];
    }

    // вызовется в момент инициализации компонента
    ngOnInit() {
        this.todos = this.todoService.getTodos();
    }

    delete(todo: Todo) {
        // от компонента todo-item мы получим задачу, которую дальше отправим в сервис
        this.todoService.deleteTodo(todo);
    }

    // выполнение задачи и далее в сервис
    toggle(todo: Todo) {
        this.todoService.toggleTodo(todo);
    }
}

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

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

Зависимость ищется снизу вверх (от дочернего к родит-му компоненту).

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

@NgModule({
    imports: [BrowserModule],
    providers: [TaskService],

Далее добавляем сервис через конструктор:

export class TodoFormComponent {

//добавим конструктор для того чтобы получить service
constructor(private taskService: TaskService) {

}

     Внедрение зависимостей внутрь отдельного компонента, на примере Http

Посредством свойстваproviders:

@Component({
    providers: [Http],
    selector: '[data-hello]'
})
class MyClass {
data: string;
    consructor(http: Http){
        http.het('googgle.com')
        .map((res: Response) => res.text())
        .subscribe((data: string) => this.data = data)
    }
}

     Декортатор @Injectable или Использование сервиса внутри сервиса

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

@Injectable()
export default class MyService {
    constructor(myService: MyService) {

    }
    //...

Декортатор @Injectable делает служебные классы (сервисы) видимыми для механизма внедрения зависимостей:

Как использовать сервис внутри сервиса: можно за-Inject-ть нужный сервис как обычно через конструктор и работаем с сервисом внутри компонента-сервиса. Но при этом angular выдаст ошибку. Если у компонента есть декоратор, то в него можно спокойно инжектить сервисы и т.д. Но у самого сервиса (класса) нет декоратора и мы не сможем заинжектить в него другой сервис. И для того чтобы разрешить инжект в сервисе команда angular придумала специальный декоратор для тех же сервисов - @Injectable(). При это не забудьте добавить сервис в providers самого модуля.

HTTP

Общение с сервером происходит в сервисе посредством HTTP. HTTP - это отдельный модуль, который не является частью angular. Все HTTP-запросы, выполняемые классом HTTP, возвращают поток экземпляров класса Response.

     Как использовать

1. Необходимо зарегестрировать http-модуль в основном модуле приложения.

//app.module.ts
import { HttpModule } from '@angular/http';

//...

@NgModule({
    imports: [BrowserModule, FormsModule, HttpModule], // что модуль импортирует
//...

2. Далее в сервисе мы можем импортировать объект Http

//todo.service.ts
    import { Http } from '@angular/http';

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


// инициализируем экземпляр Http
constructor(private http: Http) {}
// таким образом экземпляр сервиса Http будет помещен в свойство класса под названием http

Для того чтобы в нашем сервисе (todo.service.ts) иметь возможность использовать другие сервисы, необходимо добавить аннтотация с помощью декоратора injectable

// todo.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';

@Injectable()
export class TodoService {
    //..

Благодаря модулю angular-in-memory-web-api можно создать фейковый сервер.

npm i angular-in-memory-web-api --save

Для загрузки модулей мы используем system.js, поэтому в нем (systemjs.config.js) необходимо указать откуда брать модуль angular-in-memory-web-api.

Далее нужно создать класс, который будет являться БД для фейкового сервера.

/shared/data.service.ts

Через сервис http мы будем на сервер отправлять запросы (мы будем работать с restfull api):

  • post - для создания задач
  • delete - для удаления
  • put - для сохранения измененных задач

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

Для удобства создания заголовком мы импортируем два класса Headers и RequestOptions из Http

//todo.service.ts
import { Http, Headers, RequestOptions } from '@angular/http';

     Переходим к запросам

import { RequestOptions, Request, RequestMethod } from '@angular/http';

var options = new RequestOptions({
  method: RequestMethod.Post,
  url: 'https://google.com'
});

var req = new Request(options);
var myHttpRequest: Observable<Response> = http.request(req);

// сокращенная запись:
var myHttpRequest: Observable<Response> = http.post('https://google.com');

//методы класса Http возвращают наблюдаемый поток экземпляров класса Response

myHttpRequest.map(response: Response => response.json())
    .subscribe(data => console.log(data))

     Response

Http возвращают наблюдаемый поток экземпляров класса Response.

response.json():

Парсим JSON: Данные в JSON формате. Приложение должно распарсить эти данные в JavaScript объект, вызвав response.json().

https://angular.io/docs/ts/latest/guide/server-communication.html#!#parse-to-json

     Ошибки

В сервисе можно использовать специальный оператор от RxJS - catch:

// работаем в сервисе
getCars() {
    return this.http
    .get('http://localhost:3000/cars')
    .map((response: Response) => response.json()) // в angular 5 данная операция НЕ ТРЕБУЕТСЯ
    .catch((error: Response) => {
        return Observable.throw('Сервер временно недоступен. Попробуйте позже.');
    });
}

Далее нам нужно обработать ошибку в самом компоненте, где второй параметр у функции subscribe принимает ф-ю для обработки ошибки (3-й параметр - см. в коде):

// работаем в компоненте
loadCars() {
    this.carsService
    .getCars()
    .subscribe(
        (cars: Cars[]) => {
            this.cars = cars;
        },
        (error) => {
            console.log('error: ', error);
        }
        // 3-й парамет отвечает за полное выполнение STREAM
    );
}

Локальные ссылки в шаблонах

Идентификаторы с предшествующим символом решетки (#) - это такие локальные имена, которые используются для ссылок на компоненты в представлениях, чтобы в дальнейшем получить к ним доступ.

  <count
    [seconds]="125"
    #counter>
  </count>

{{ counter.seconds }}

Кроме того через локальную ссылку можно ссылаться на определенный элемент, например, абзац:

<p #tooltip >Hello?</p>

Стили компонента

Есть несколько способов определить стили внутри компонента:

     1. Свойство styles

Свойство styles декоратора @Component

@Component({
  styles: [`
    h1 {
        color: #900
    }
    p {
        color: #000
    }
  `],

     2. Свойство styleUrls

@Component({
        styleUrls: ['path-to-style.css']

     3. Внутрення таблица стилей

@Component({
  template: '
    <style>    h1 {
        color: #900
    }</style>
    <h1>Hello, world!</h1>',

Стили компонента (:host, :host-context, /deep/)

С помощью псевдо класса :host можно изменить стили элемента, в котором находится компонент (другими словами мы меняем стили селектора, который определили в компоненте).

:host {
    border: 1px solid #000;
}

:host-context - проверка родительских элементов относительно компонента на соответствие селектору указанному в параметрах (то есть, если у какого-либо элемента родителя компонента будет класс theme-yellow, то правило ниже сработает).

:host-context(.theme-yellow) h2 {
    color: yellow;
}

Как использовать /deep/: все параграфы текущего компонента и дочерних Shadow DOM получат следующие стили

:host /deep/ p {
    text-decoration: underline;
}

Соглашение об именовании в angular 2

Имя модуля требуется указывать в верблюжьей нотации, а также суффикс типа (например, Pipe): NumberOnlyPipe

Маршрутизация

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

     Порядок настройки роутинга

//app.module.ts
//I
import { RouterModule } from '@angular/router'

@NgModule({
  imports: [
      //II
    RouterModule.forRoot([{
            path: 'welcome',
            component: WelcomeComponent
        }, {
            path: '',
            redirectTo: 'welcome',
            pathMatch: 'full'
        }, {
            path: '**',
            component: PageNotFoundComponent
        }
    ]),

/// III (index.html)

<router-outlet></router-outlet>

     Вторичные роуты

Допустим у нас есть app/products/product.module.ts, которые содержит общий модуль для работы с продуктами.

//app.module.ts
@NgModule({
  imports: [
    ProductModule,


// product.module.ts
@NgModule({
  imports: [
    SharedModule,
      RouterModule.forChild([
          {
            // переход спокойно сработает с ./app/app.component.html
            /*
            Это происходит потому, что angular merge данный роутинг с основным роутингом,
            причем второстепенные модули он merge вверх, поэттому ** из основного модуля не сработают
            */
            path: 'products',
            component: ProductListComponent
          }
      ])
  ],

Routing
DeborahK/Angular-Routing

<base href='/'> - сообщает браузеру путь, по которому он должен идти, чтобы загрузить внешние стили, катринки и т.д.

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

import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated';

@Component({
  selector: 'my-app',
  directives: [ROUTER_DIRECTIVES],
  providers: [ROUTER_PROVIDERS],

Документация с актуальными мудулями: https://angular.io/docs/ts/latest/guide/router.html

Опции маршрута в устаревшей версии прописываются в декораторе:

@RouteConfig([
    {   path: '',
        name: 'Home',
        redirectTo: ['TasksComponent']
    },
    //....

Актуально:

 //app.module.ts
import { RouterModule, Routes } from '@angular/router';

// определение маршрутов
const routes: Routes = [
    // pathMatch:'full' указывает, что запрошенный адрес должен полностью соответствовать маршруту
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    { path: 'dashboard',  component: DashboardComponent },
    { path: 'detail/:id', component: HeroDetailComponent },
    { path: 'heroes',     component: HeroesComponent }
];

@NgModule({
    //Чтобы применить маршруты:
    imports: [ RouterModule.forRoot(routes) ],
    exports: [ RouterModule ]
})
export class AppRoutingModule {}

     Директивы RouterOutlet, RouterLink и routerLinkActive

RouterOutlet - директива (<router-outlet>) станет заполнителем, где роутер отобразит view (при этом все предыдущие компоненты будут удалены).

Для определения адресов ссылок применяется директива routerLink. Для стилизации активных ссылок применяется специальная директива routerLinkActive. Активная ссылка с директивой [routerLink] получает класс router-link-active.

Значение {exact:true} указывает на то, что для установки активной ссылки будет применяться полное соответствие:

<a routerLink=""
    routerLinkActive="active"
    [routerLinkActiveOptions]="{exact:true}">
        Главная
</a>
<a routerLink="/about"
    routerLinkActive="active">
        О сайте
</a>

     Принудительная навигация (navigate) в компоненте

Как видите, методу navigate передается строка, содержащая полный путь.

gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero.id]);

    // { path: 'detail/:id', component: HeroDetailComponent },

}

2 пример:

app/user/login.component.ts

import { Router } from '@angular/router';

export class LoginComponent {

    constructor(private router: Router ) { }

    login(loginForm: NgForm) {
        // здесь welcome подменит только основной сегмент
        // (/welcome) из www.test.com/welcome(second:value)
        this.router.navigate(['/welcome']);

        // здесь welcome подменит все
        // (/welcome(second:value)) из www.test.com/welcome(second:value)
        this.router.navigateByUrl('/welcome');

Навигация с queryParams и хэшем

// cars?color=pink&year;=1975#pic

openPage() {
    this.router.navigate(['./cars', 8, 'opel'], {
        // ?color=pink&year;=1975
        queryParams: {
        color: 'pink',
        year: 1975
        },
        // хэш: #pic
        fragment: 'pic'
    });
}

     Динамические параметры

<button (click)="gotoDetail(i)">View Details</button>
gotoDetail(index: number): void {
//Переходим к компоненту Timer с параметром id
    this.router.navigate(['Timer', { id: index }]);
}

Или

<a *ngFor="let product of products"
  [routerLink]="['/product-details', product.id]">
  {{ product.name }}
</a>

     Настраиваем и получаем параметры

Сейчас актуально: Получение параметров routeparams посредством сервиса: ActivatedRoute.
Deprecated: RouteParams.

ActivatedRoute - содержит информацию о маршруте связанную с компонентом, который загружен в outlet

// определение маршрутов
const appRoutes: Routes =[
    { path: 'item/:id', component: MyComponent},
    {
        path: 'products/:id/edit',
        component: ProductEditComponent
    }

Mы сможем обратиться к компоненту с запросом типа /item/7, и число 7 будет представлять параметр id.

<a [routerLink]="['item', '7']">item 7</a>

<a class="btn btn-primary" [routerLink] = "['/products', product.id, '/edit']">
    Edit
</a>
import { Component, OnDestroy} from '@angular/core';
import { ActivatedRoute} from '@angular/router';
import {Subscription} from 'rxjs/Subscription';

export class MyComponent implements OnDestroy {
    constructor(private activateRoute: ActivatedRoute){
        this.subscription = activateRoute.params.subscribe(params=>this.id=params['id']);
    }
    ngOnDestroy(){
        this.subscription.unsubscribe();
    }
}

Метод subscribe() позволяет установить подписку на изменение параметра маршрута.

Поэтому при переходе на компонент с разных ссылок (с разными параметрами) всегда будет актуальное значение id.

Про маршрутизацию можно посмотреть также на русскоязычном сайте metanit Параметры строки запроса


     Получаем параметры текущего route

С использованием snapshot:

Однако учтите, что snapshot не следит за изменениями при route.

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

export class ProductDetailComponent implements OnInit {
    constructor(private productService: ProductService,
                private route: ActivatedRoute) {
        // плохая практика вызывать в конструкторе "тяжелые" операции (в том числе и связанные с route)
        // поэтому лучше его использовать в ngOnInit()
    }

    ngOnInit(): void {
        // первый вариант вытащить параметры от роутинга
        // snapshot - параметры route на момент инициализации
        let id = +this.route.snapshot.params['id'];
        this.getProduct(id);
    }

    getProduct(id: number) {
        this.productService.getProduct(id).subscribe(
            product => this.product = product,
            error => this.errorMessage = <any>error);
    }
}

     Подписываемся на изменения параметров route при помощи route.params.subscribe:

В 4-м ангуляре route.params заменена на route.paramMap

    ngOnInit(): void {
    // route.params возвращает Observable, а значит на него можно подписаться
    // и мы можем следить за изменениям наших параметров, и перерисовывать страницу

    // здесь мы работаем с параметрами вида: { path: 'item/:id',
    this.route.params.subscribe(
        params => {
            let id = +params['id'];
            this.getProduct(id);
        }
    );

    // подписываемся на GET параметры
    this.route.queryParams.subscribe((params: Params) => {
        this.color = params['color'];
        this.year = params['year'];
    })


}

     Дополнительные параметры

<a
   [routerLink] = "['/products', product.id, 'edit', { name: 'John', city: 'Moscow' }]">
    Edit
</a>

В браузере: http://localhost:3000/products/1/edit;name=John;city=Moscow

Получить в компоненте можно точно также как и для основных параметров (см. пример выше).

     Строка запроса

http://localhost:3000/item?name=Vasya&year;=200 часть name=Vasya&year;=200 является строкой запроса.

constructor(private route: ActivatedRoute){

Используем route.queryParams

Пример

Передаем:

<a [routerLink] = "['/products', product.id]"
    [queryParams] = "{filterBy: listFilter, showImage: showImage}"
>
{{product.productName}}
</a>

В браузере: http://localhost:3000/products/1?filterBy=leaf&showImage;=false

Сохранили параметры, пришедшие на странцу выше и отдали обратно:

<a class="btn btn-default" [routerLink] = "['/products']"
    [preserveQueryParams] = "true"
    >
    Back
</a>

В 4-м ангуляре preserveQueryParams заменена на queryParamsHandling = "preserve"

Читаем данные в компоненте:

ngOnInit(): void {

    this.listFilter = this.route.snapshot.queryParams["filterBy"] || '';
    this.showImage = this.route.snapshot.queryParams["showImage"] === 'true';

     Route Resolvers

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

Это происходит из-за того, что сервис получает, например, информацию о продукте в ngOnInit асинхронно, то есть для этого требуется время, а компонент уже загружен:

    ngOnInit(): void {
    this.route.params.subscribe(
        params => {
            let id = +params['id'];
            this.getProduct(id);
        }
    );
}

Чтобы этого избежать мы можем загружать данные до того как перейдем на компонент при помощи Route Resolvers. Route Resolvers implements как отдельный сервис.

//app/products/product-resolver.service.ts
import { Injectable } from "@angular/core";
import { IProduct } from "./product";
import {Resolve, ActivatedRouteSnapshot, RouterStateSnapshot} from "@angular/router";
import {Observable} from "rxjs/Rx";
import {ProductService} from "./product.service";

@Injectable()
// возвращаться будет IProduct
export class ProductResolver implements Resolve<IProduct> {
    constructor(private productService: ProductService){ }
    // route - текущее состояние активного route
    // state - текущее состояние всего route
    resolve(route: ActivatedRouteSnapshot,
            state: RouterStateSnapshot): Observable<IProduct> {
        let id = +route.params['id'];

        let product = this.productService.getProduct(id);
        return product;
    }
}

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

//app/products/product.module.ts
@NgModule({
  imports: [
    RouterModule.forChild([{
        {
            path: 'products/:id',
            component: ProductDetailComponent,
            // key - имя того что ресолвить (product), ProductResolver - resolver
            resolve: {product: ProductResolver}
        },
    ],
  providers: [
    ProductResolver,
    ProductService
  ]

Модифицируем компонент для работы с resolver:

//products/product-detail.component.ts

export class ProductDetailComponent implements OnInit {
    pageTitle: string = 'Product Detail';
    product: IProduct;
    errorMessage: string;

    constructor(private route: ActivatedRoute) {}

    ngOnInit(): void {
        // продукт загруженные resolver
        this.product = this.route.snapshot.data['product'];
    }

}

Подписываемся на изменения (нужно если с одного компонента переходим на него же, но с другими данными):

/product-edit.component.ts
    ngOnInit(): void {
        this.route.data.subscribe(
            data => {
                let product = data['product'];
                this.onProductRetrieved(product);
            }
        );

    }

    onProductRetrieved(product: IProduct): void {
        this.product = product;
    }

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

     ... (многоточие), маршрут во вложенном компоненте

... (многоточие) в конце маршрута, указывает маршрутизатору Router, что он должен смотреть определение маршрута во вложенном компоненте.

Получаем параметры в компоненте:

// программный переход в
this.router.navigate(['TimerComponent', 'TaskTimer', { id: index }]);
import { RouteParams, CanReuse, OnReuse } from '@angular/router-deprecated';

constructor(
    private routeParams: RouteParams) {
}

ngOnInit(): void {
    let taskIndex = parseInt(this.routeParams.get('id'));

     интерфейс CanActivate

Интерфейс CanActivate расширяет класс для решения задачи по активации маршрута.

Метод canActivate - возвращает логическое значение, указывающее на необходимость активации компонента. Удобно для проверки паролем и т.д.

// auth-guard-admin.service.ts
/*
    Guard - механизм для выполнения проверок перед активацией и деактивацией маршрута

    CanActivate - Определяет возможность активации маршрута
    CanActivateChild - Определяет возможность активации дочерних маршрутов текущего маршрута
    CanDeactivate - Определяет можно ли уйти с текущего маршрута
    CanLoad - Определяет может ли модуль загрузиться с использованием lazy loading

    Установка объектов Guard происходит при настройке маршрутизации.
*/

@Injectable()
export class AuthGuardAdmin implements CanActivate {
    // Observable<boolean>|Promise<boolean>|boolean - возможные результаты работы метода
    // Если return true, то маршрут будет активирован, иначе - нет
  constructor(public auth: AuthService, private router: Router) {}

  canActivate() {
    return this.auth.isAdmin;
  }

}
// routing.module.ts
const routes: Routes = [
    //...
    { path: 'delete-article', component: DeleteArticleComponent, canActivate: [AuthGuardAdmin] },
    //...

И не забудьте добавить AuthGuardLogin в providers:

// app.module.ts
//...
providers: [
    AuthGuardLogin
//...

     Интерфейс onActivate

     Использованы материалы в том числе:
codedojo.ru

Использование относительного пути


<button (click)="goToPhrasesList()">Go Back</button>
goToPhrasesList() {
    let pId = this.phrase ? this.phrase.id : null;
    // использование относительного пути при перенаправлении пользователя.
    // ../ подняться на уровень выше
    this.router.navigate(["../", { id: pId, p1: "test", p2: 123 }], { relativeTo: this.activatedRoute });
}

// http://localhost:3000/phrases;id=3;p1=test;p2=123/3
// на
// http://localhost:3000/phrases;id=3;p1=test;2=123

Шаблон Observable

Если уже совсем простым языком:

Экземпляр наблюдаемого объекта (observable) имеет подписку (subscribe) с определенным функционалом; наблюдатель (observer) при помощи, например, метода next говорит об изменении состояния наблюдаемого объекта, благодаря чему вызывается функционал из подписки (subscribe) наблюдаемого объекта. Метод next наблюдателя (observer) может передавать параметры, которые затем принимает подписка (subscribe) наблюдаемого объекта (observable).

Observable (наблюдаемый объект) - источник асинхронного события; Observable сообщает об изменении своего состояния объекту - Observer (наблюдатель или подписчик).

В независимости от того переданы данные или нет объект Observable может быть оставлен в любое время(в отличие от тех же Promise).

RxJS - библиотека, необходима для создания Observable-объектов.

В отличие от Promise (где сработает только 1 раз) нижеприведенный код будет работать как задумано (https://jsfiddle.net/3cz80mjn/):


//  в этом примере мы создали наблюдаемый объект и подписались на его события
// observable это и есть stream
var observable = Rx.Observable.create(observer => {

    observer.next('Асинхронная операция 0');

    setInterval(() => {
        observer.next('Асинхронная операция');
    }, 1000);

});

observable.subscribe(response => {
    console.log('response: ', response);
})

Observable производят непрерывный поток событий (тоже самое что и коллекция объектов), на которые можно подписать абонентов. Подписчики получают уведомления об этих событиях и реагирует на них. Прежде чем поток достигнет подписанных на него наблюдателей его можно предварительно обработать: при помощи методов, например, map(), filter().

Реактивное программирование - подписка на асинхронные события и преобразование потоков наблюдаемых событий.

Observable с использованием map, filter b subscribe https://jsfiddle.net/dnzl/g2juyk7d/5/

     Метод map

Метод map в реактивном программировании аналогичен функциональному методу map:

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

var nums = [11, 21, 31, 41];
var results = nums.map(function(num, index, arr) {
    return num * 2;
});

// Исходный массив nums не изменяется
console.log(nums); // [11, 21, 31, 41]
// результат выполнения map, записанный в переменную
console.log(results); // [ 22, 42, 62, 82 ]
var numbers = [1, 4, 9];
var roots = numbers.map(Math.sqrt);

// теперь roots равен [1, 2, 3], а numbers всё ещё равен [1, 4, 9]
console.log('roots: ', roots); // [ 1, 2, 3 ]
https://jsfiddle.net/y08234mv/1/
var objs = [
    {which: 1},
    {which: 0},
    {data: 0}
];
var objsWithWhich = objs.map(val => val.which);

console.log('objsWithWhich: ', objsWithWhich); // [ 1, 0, undefined ]

В отличие от forEach при использовании map вам становится доступен chaining.

http://jsraccoon.ru/fn-array-methods

     Метод filter

Метод filter служит для фильтрации массива по условию, заданным в callback функции. В результате создаётся новый массив, куда добавляются все элементы прошедшие провеку колбэком.

function isBigEnough(value) {
  return value >= 10;
}
var filtered = [1, 5, 8, 13, 454].filter(isBigEnough);

console.log('filtered: ', filtered); // [ 13, 454 ]

     Строгая типизация

Если вы взгляните на исходники Angular, то увидите, что Http модуль содежит метод request (https://github.com/angular/angular/blob/2.4.1/modules/%40angular/http/src/http.ts#L111).

Все другие методы обернуты этим request'ом. Также вы можете увидеть, что request возвращает Observable c generic'ом класса Response. Response класс является частью Http модуля.

import { HttpModule, Http, Response } from '@angular/http';
...
theDataSource: Observable<Response>;

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

theDataSource: Observable<any>;

     pipe async

Вывести Observable на странице можно при помощи фильтров.

<pre>{{latestNoteBooks$ | async | json}}</pre>

RxJs и Observable

Observable

const observer = {
    next: function nextCallback(data) {
        console.log(data)
    },
    complete: function completeCallback() {
        console.log('Done')
    },
    error: function errorCallback(err) {
        console.log('error: ', err)
    }
};

// эта ф-я должна взять предыдущий Observable (его значение), создать новый Observable, на
// который потом кто-то сможет подписаться
function mapFn(transformationFn) {
    const inputObservable = this;
    const outputObservable = createObservable(function subscribe(obs) {

        inputObservable.subscribe({
            next: (x) => obs.next(transformationFn(x)), // transformationFn - преобразовываем данные
            complete: () => obs.complete(),
            error: (err) => obs.error(err)
        });

    });

  return outputObservable;
};

// ф-я создает observable
function createObservable(subscribeFn) { // subscribeFn будет emit наши данные
    return {
        map: mapFn, // одинаковы для каждого объекта observable
        subscribe: subscribeFn // изначально: function startReceivingData(obs) {......} // функция подписки
  }
};

// intervalObservable - это объект, созданный при помощи createObservable
const intervalObservable = createObservable(function startReceivingData(obs) {
  let counts = 0;
  const intervalId = setInterval(() => {
      counts++;
      obs.next(counts);

      if (counts > 5) {
            clearInterval(intervalId);
            obs.complete();
        }
    }, 300);
});

intervalObservable
    .map(x => x*10) // возвращает новый Observable
    .subscribe(observer); // observer подписывается на данные или observer - это объект, который получает данные

/*
    10
    20
    30
    40
    50
    60
    Done
*/

jsfiddle.net

     RxJs операторы - 120+

// позволяют создавать observable
from()          // из, например, [
fromEvent()     // observable из event
fromPromise()   // observable из Promise

// методы
map()           // изменяем конкретные данные
filter()        // фильтрация
take()          // позволяет взять несколько значений

//
scan()      // позволяет накапливать данные
delay()     // откладывать emit

     Какие задачи легко решать с RxJs

  • Объединение нескольких событий вместе
  • Фильтрация событий (delay, debounce, throttle)
  • Управление асинхронными действиями
  • Когда нужна отмена запросов

     Какие задачи не нужно решать с RxJs

  • Используйте там где это имеет смысл
  • RxJs не нужен, чтобы обрабатывать клик по кнопке
  • RxJs не нужен, чтобы submit форму на бекенд
  • RxJs не фреймворк, не нужно писать вс приложение, как один Observable

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

Баррель

Баррель - это библиотечный модуль Angular, объединяющий модули с одинаковым назначением.

Как подключать одно в другое

Компоненты и директивы и фильтры:
Все директивы, компоненты и фильтры объявляются в свойстве declarations основного модуля.

Сервисы
Сервисы подключаются в providers и внедряются через конструктор:

Соглашение об именовании

  • Имена фалов и папок: в нижнем регистре и с дефисом между словами.
  • К фафлам директов, компонентам, фильтрам и сервисам следует добавлять суффикс определяющий их тип: slider.component.ts
  • Селекторы директив, имена фильтров, модули задаются в верблюжьейНотации
  • Селекторы компонентов в нижнем-регистре-с-дефисом-между-словами

Анимация в angular

  • 1. Необходимо подключить модуль для работы с анимацией: BrowserAnimationsModule
  • 2. Необходимо установить (package.json) дополнительную библиотеку: npm i @angular/animations --save @angular/animations работает через web animation api
  • 3. Для кроссбраузерность потребуется полифилл (файл src/polyfills.ts); в этом файл потребуется раскомментировать // import 'web-animations-js';, и далее установить данный пакет.
  • 4. Необходимо задать стили блоку, которые вы собираетесь анимировать:
    Как работает анимация в ANGULAR: мы задаем состояния элементу (разные стили) и переключаемся между ними.
//test-animatiom.component.ts
import { Component, OnInit } from '@angular/core';

// для анимации
import { trigger, state, style, transition, animate } from "@angular/animations";

@Component({
    selector: 'app-test-animation',
    templateUrl: './test-animation.component.html',
    styleUrls: ['./test-animation.component.scss'],

    // animations - массив, где хранится состояния нашей анимации
    animations: [
        // trigger - даем название анимации
        // 1 param: название анимации
        // 2 param: массив состояний, например, состояние до анимации, состояние после анимации
        trigger('clickOnDiv', [
            // state - определяем состояние
            // 1 param: название состояния
            // 2 param: стили
            state('start', style({
                backgroundColor: 'green',
                width: '200px',
                height: '200px'
            })),
            state('end', style({
                backgroundColor: 'red',
                width: '400px',
                height: '400px'
            })),
            // transistion отвечает за 'переходы'/поведение анимации
            // 1 param: описываем из какого в какое состояние должна происходить анимация
            // 2 param: ф-я animate
                // для ф-и animate:
                // 1 param: время анимации ||
                // 1 param: "скорость, задержка, и тип анимации"
            transition('start => end', animate(1500)),
            transition('end => start', animate('500ms 1000ms ease-out'))
        ])
    ]
})
export class TestAnimationComponent implements OnInit {

    // анимируем, например, эта переменная будет хранить значение состояния
    clickDiv = 'start';

    constructor() {}

    ngOnInit() {}

    changeStateOnDiv() {
        // меняем состояние анимации
        this.clickDiv = 'end';
        setTimeout(() => {
            this.clickDiv = 'start';
        }, 3000)
    }

}
// двойная анимация (например, увеличиваем и уменьшаем)
transition('start <=> end', animate(2500)),
<!-- посредством trigger clickOnDiv указываем, что к данном div Необходимо применить анимацию  -->
<!-- [@clickOnDiv]="'start'" указываем состояние -->
<!-- OR: [@clickOnDiv]="clickDiv" указываем состояние -->
<!-- меняем состояние: (click)="clickDiv = 'end'" -->

<!--
<div class="test-ani" [@clickOnDiv]="clickDiv" (click)="clickDiv = 'end'">
</div>
-->

<div class="test-ani"
     [@clickOnDiv]="clickDiv"
     (click)="changeStateOnDiv()">
</div>

     Специальные символы

void

Про void мы узнали выше. Angular team предоставляет специальный state - void. void овечает за отсутствие элемента в DOM.

*

* - означает любой state

transition('void => *', [
    style({
    //...

:enter

Для данного состояния 'void => *' есть соответствующий ярлык :enter

transition(':enter', [
    style({
    //...

:leave

Для данного состояния '* => void' есть соответствующий ярлык :leave

transition(':leave', [
    style({
    //...

* как default ширина/высота

// меняем ширину, при этом используя * возвращаем к ширину default
trigger('triggerForDivWidth', [
    transition('* => *', [
        animate(1000, style({
            width: '0px'
        })),
        animate(1000, style({
            // как анимации узнать ширину из inline style ? при помощи символа *
            // котор подразумевает default ширину/высоту
            width: '*'
        }))
    ])
])

     Детальное управление анимацией

group()
1 param - массив ф-й animate, котор. начинаются в одно и тоже время, но длительность может отличаться

keyframes() - предназначена для декомпозиции времени анимации, чтобы мы могли использовать разные свойства

см. код

     События

Задание событий по старту или концу анимации и прослушка этих событий.

<div
    class="alert alert-danger"
    *ngIf="isVisible2"
    @triggerForDiv2

    (@triggerForDiv2.start) ="onStart($event)"
    (@triggerForDiv2.done) ="onEnd($event)"

>
    Lorem ipsum dolor sit amet.
</div>

Полезные ресурсы

Более подробно об Angular вы можете узнать на сайте dnzl.ru (Angular). На dnzl.ru присутствует целый раздел посвященный непосредственно Angular.

  • angular.io
  • angular на medium.com
  • metanit.com: angular2

Комментарии к статье

аватарка пользователя
2017-07-05
test

test

аватарка пользователя
2017-10-24
Марк

Жаль что автор не указан ! Это САМАЯ КРУТАЯ СТАТЬЯ КОТОРУЮ Я НАШЕЛ !!! СПАСИБО ОГРОМНОЕ АВТОРУ !!!

аватарка пользователя
2017-10-24
dnzl

не за что