Книга: Angular и TypeScript. Сайтостроение для профессионалов
Назад: 5. Привязки, наблюдаемые объекты и каналы
Дальше: 7. Работа с формами

6. Реализация коммуникации между компонентами

В этой главе:

создание слабо связанных компонентов;

• передача данных компонентом-предком компоненту-потомку и наоборот;

• реализация шаблона проектирования;

• посредник для создания компонентов, которые можно использовать повторно;

• жизненный цикл компонента;

• разбираем определение изменений.

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

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

Мы продолжим тему, приведя пример использования шаблона проектирования «Посредник» с целью реализовать обмен данными между компонентами, не связанными отношениями «предок — потомок». Посредник — это, возможно, самый важный шаблон в любом фреймворке, в основе которого лежат элементы. Наконец, мы рассмотрим жизненный цикл компонента Angular и привязки, пригодные для написания необходимого приложению кода, перехватывающего важные события во время создания элемента, а также его жизненного цикла и разрушения.

6.1. Коммуникация между компонентами

На рис. 6.1 показано представление, состоящее из нескольких компонентов, которые пронумерованы и имеют разные формы (чтобы было проще на них ссылаться). Отдельные компоненты содержат другие элементы (назовем внешние компоненты контейнерами), другие же являются компонентами одного уровня. Чтобы абстра­гироваться от конкретных фреймворков пользовательского интерфейса, мы избегали использования таких элементов HTML, как поля ввода, выпадающие списки и кнопки, но вы можете перенести этот пример на представление вашего собственного приложения.

73597.png 

Рис. 6.1. Представление состоит из компонентов

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

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

6.1.1. Входные и выходные свойства

Представьте, что компонент Angular — это черный ящик, имеющий несколько выходов. Часть их отмечены как @Input(), а другие — как @Output(). Вы можете создать компонент, который будет иметь столько входных и выходных свойств, сколько захотите.

Если компонент Angular должен получать значения из внешнего мира, то можно связать производителей этих значений с соответствующими входными свойствами компонента. От кого получены данные значения? Элемент не должен это знать — ему достаточно информации о том, что с ними делать при получении.

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

Реализуем описанные принципы. Сначала вы создадите компонент OrderComponent, который может получать запросы на заказы из внешнего мира.

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

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

В листинге 6.1 показаны два компонента: AppComponent (предок) и OrderComponent (потомок). Второй имеет два свойства, stockSymbol и quantity, они отмечены аннотациями @Input. Компонент AppComponent позволяет пользователям ввести комбинацию символов, представляющую название акции, которая будет передана компоненту OrderComponent с помощью привязок.

Вы также будете передавать компоненту OrderComponent значение свойства quantity; но здесь вы не будете применять привязки, чтобы увидеть случай, когда предку нужно передать потомку значение, которое не будет изменяться. Вы не будете пользоваться механизмом привязок, не окружая атрибут quantity тегом <order-processor> с квадратными скобками.

Листинг 6.1. Содержимое файла input_property_binding.ts

73605.png 

76455.png 

СОВЕТ

Поскольку вы не будете использовать привязку для атрибута quantity, значение 100 поступит в компонент OrderComponent как строка (все значения атрибутов HTML являются строками). Если хотите сохранить типы, то задействуйте привязки, например, так: [quantity]="100".

примечание

Если измените значение свойств stockSymbol или quantity в компоненте OrderComponent, то это не затронет значения свойств компонента-предка. Привязки свойств являются односторонними: от предка к потомку.

На рис. 6.2 показано окно браузера после того, как пользователь введет значение IBM. Компонент OrderComponent получил входные значения.

ch06_02.tif 

Рис. 6.2. Компонент OrderComponent получил значения

 

Следующий вопрос заключается в том, как компонент определяет момент, когда изменяется одно из его входных свойств? Самым простым способом будет изменить входное свойство так, чтобы оно стало сеттером. Вы используете stockSymbol в шаблоне компонента, поэтому вам также понадобится геттер. Поскольку ваш сеттер открытый, задайте переменной имя _stockSymbol и сделайте ее закрытой (листинг 6.2).

Листинг 6.2. Добавление сеттера и геттера

private _stockSymbol: string;

@Input()

set stockSymbol(value: string) {

    this._stockSymbol = value;

    if (this._stockSymbol != undefined) {

      console.log(`Sending a Buy order to NASDAQ:

        ${this.stockSymbol} ${this.quantity}`);

    }

}

get stockSymbol(): string {

    return this._stockSymbol;

}

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

примечание

В подразделе 6.2.1 мы опишем, как перехватывать изменения входных свойств, не используя сеттеры.

В главе 3 мы показали, как передавать параметры в компонент, используя ActivatedRoute. В этом сценарии параметры передаются с помощью конструктора. Привязка к параметрам @Input() — решение для передачи данных от предка к потомку, оно работает только в том случае, если компоненты располагаются внутри одного маршрута.

Выходные свойства и пользовательские события

Компоненты Angular могут отправлять пользовательские события, используя объект EventEmitter. Эти события можно обрабатывать либо в компоненте, либо с помощью его предков. Данный объект представляет собой подкласс Subject (реализован в библиотеке RxJS), который может быть как наблюдаемым потоком, так и наблюдателем. Другими словами, EventEmitter может отправлять пользовательские события с помощью метода emit(), а также работать с наблюдаемыми потоками, задействуя метод subscribe(). Поскольку этот раздел посвящен отправке данных элемента во внешний мир, мы сконцентрируемся на теме отправки пользовательских событий.

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

Создадим компонент PriceQuoterComponent, который реализует подобную функциональность. Для этого примера вам нужно не связываться с серверами, а только эмулировать изменяющиеся цены, используя генератор случайных чисел. Отображать изменяющиеся цены в компоненте PriceQuoterComponent можно довольно прямолинейно: вы свяжете свойства stockSymbol и lastPrice с шаблоном компонента.

Внешний мир вы оповестите путем отправки пользовательских событий с помощью свойства элемента с аннотацией @Output. Вы будете отправлять событие, как только изменится цена. Оно будет нести полезную нагрузку: объект, содержащий комбинацию символов для акции и ее последнюю цену. Эта функциональность реализована в следующем сценарии (листинг 6.3).

Листинг 6.3. Содержимое файла output-property-binding.ts

76903.png 

76993.png 

Обработчик событий получает объект типа IPriceQuote, и вы извлекаете из него значения свойств stockSymbol и lastPrice. Если запустите этот пример, то увидите, как цены обновляются каждую секунду в компонентах PriceQuoterComponent (затененный фон) и AppComponent (белый фон), как показано на рис. 6.3.

ch06_03.tif 

Рис. 6.3. Запуск примера работы с выходными свойствами

 

СОВЕТ

По умолчанию имя пользовательского события совпадает с именем выходного свойства, в нашем случае это lastPrice. Если хотите генерировать событие с другим именем, то укажите его в качестве аргумента для аннотации @Output. Например, чтобы сгенерировать событие с именем last-price, объявите выходное свойство как @Output('last-price') lastPrice;.

В листинге 6.3 вы создаете компонент Angular PriceQuoterComponent, который содержит пользовательский интерфейс. Но для бизнеса может понадобиться функциональность получения price-quote без интерфейса, чтобы использовать ее как в приложениях для трейдеров, так и в крупных панелях инструментов. Вы можете реализовать ту же функциональность в качестве внедряемого сервиса, как сделали для сервиса ProductService в проекте онлайн-аукциона.

Поднятие событий

На момент написания данной книги Angular не предлагает синтаксис, поддерживающий поднятие событий. Для компонента PriceQuoterComponent это значит следующее: если вы хотите отслеживать событие last-price не в самом компоненте, а в его предке, то событие туда не поднимется. В следующем фрагменте кода событие last-price не достигнет элемента <div>, поскольку он является предком элемента <price-quoter>:

<div (last-price)="priceQuoteHandler($event)">

  <price-quoter ></price-quoter>

</div>

Если для вашего приложения важна поддержка поднятия событий, то не используйте переменную EventEmitter; вместо этого задействуйте нативные события модели DOM. В следующем примере показана еще одна версия компонента PriceQuoterComponent, в которой поднятие событий обрабатывается без EventEmitter:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { NgModule, Component, ElementRef }        from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

interface IPriceQuote {

  stockSymbol: string,

  lastPrice: number

}

 

@Component({

  selector: 'price-quoter',

  template: `PriceQuoter: {{stockSymbol}} \${{price}}`,

  styles:[`:host {background: pink;}`]

})

class PriceQuoterComponent {

  stockSymbol: string = "IBM";

  price:number;

  constructor(element: ElementRef) {

    setInterval(() => {Inter-component communication

      let priceQuote: IPriceQuote = {

        stockSymbol: this.stockSymbol,

        lastPrice: 100*Math.random()

      };

      this.price = priceQuote.lastPrice;

      element.nativeElement

          .dispatchEvent(new CustomEvent('last-price', {

            detail: priceQuote,

            bubbles: true

          }));

    }, 1000);

  }

}

 

@Component({

  selector: 'app',

  template: `

    <div (last-price)="priceQuoteHandler($event)">

      <price-quoter></price-quoter>

    </div>

    <br>

    AppComponent received: {{stockSymbol}} \${{price}}

  `

})

class AppComponent {

  stockSymbol: string;

  price:number;

  priceQuoteHandler(event: CustomEvent) {

    this.stockSymbol = event.detail.stockSymbol;

    this.price = event.detail.lastPrice;

  }

}

 

@NgModule({

  imports:      [ BrowserModule],

  declarations: [ AppComponent, PriceQuoterComponent],

  bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

В предыдущем приложении Angular внедрил ссылку на элемент модели DOM, который представляет собой элемент <price-quoter>, с помощью ElementRef, а затем путем вызова метода element.nativeElement.dispatchEvent() было отправлено пользовательское событие. Поднятие здесь сработает, но помните, что этот код может не функционировать в ряде браузеров и для всех отрисовщиков, не поддерживающих HTML.

6.1.2. Шаблон «Посредник»

Когда вы разрабатываете пользовательский интерфейс на основе компонентов, каждый из них должен работать самостоятельно и не полагаться на существование других UI-элементов. Такие слабо связанные компоненты можно реализовать с помощью шаблона проектирования «Посредник», который, согласно «Википедии», «обеспечивает взаимодействие множества объектов» (). Мы объясним, что это значит, с помощью аналогии с детским конструктором.

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

примечание

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

Снова взглянем на рис. 6.1. Каждый компонент, за исключением 1, имеет предка (контейнер), который играет роль посредника. Посредник верхнего уровня — это контейнер 1, он отвечает за то, что компоненты 2, 3 и 6 могут общаться друг с другом, если нужно. С другой стороны, компонент 2 является посредником для компонентов 4 и 5. Компонент 3 является посредником для компонентов 7 и 8.

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

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

Посредник должен знать, какой элемент может размещать заказы и как передать ему название акций и требуемое количество. На рис. 6.4 показано, как компонент AppComponent может быть посредником в коммуникации между PriceQuoterComponent и OrderComponent.

73646.png 

Рис. 6.4. Посредник при коммуникации

примечание

Отправка событий работает в широковещательном режиме. Компонент PriceQuoterComponent отправляет события с помощью свойства @Output, не зная, кто будет получать события. Компонент OrderComponent ожидает изменения значения в свойстве @Input — это сигнал о размещении заказа.

Чтобы показать шаблон «Посредник» в действии, напишем небольшое приложение, состоящее из двух компонентов, показанных на рис. 6.4. Вы можете найти это приложение в каталоге mediator, в котором содержатся следующие файлы:

stock.ts — интерфейс, в котором определяется объект value, представляющий собой акцию;

• price-quoter.ts — компонент PriceQuoterComponent;

• order.ts — компонент OrderComponent;

• mediator.ts — компоненты PriceQuoterComponent и OrderComponent.

Вы будете использовать интерфейс Stock в двух случаях:

чтобы представить полезную нагрузку события, отправленного компонентом PriceQuoteComponent;

• для представления данных, переданных компоненту OrderComponent с помощью привязки.

Далее рассмотрим содержимое файла stock.ts (листинг 6.4).

Листинг 6.4. Содержимое файла stock.ts

export interface Stock {

  stockSymbol: string;

  bidPrice: number;

}

Предположим, вы хотите использовать SystemJS для динамической компиляции кода TypeScript. По умолчанию SystemJS превратит содержимое файла stock.ts в пустой модуль stock.js, и вы получите ошибку, когда загрузчик SystemJS попробует импортировать его. Вам нужно дать SystemJS знать о том, что Stock следует рассматривать как модуль. Это можно сделать, сконфигурировав SystemJS с помощью метааннотации, как показано в следующем фрагменте кода, взятом из файла systemjs.config.js:

packages: {...},

meta: {

  'app/mediator/stock.ts': {

    format: 'es6'

  }

}

Компонент PriceQuoteComponent, показанный далее (листинг 6.5), имеет кнопку Buy (Купить), а также выходное свойство buy. Оно отправляет событие buy только в том случае, когда пользователь нажимает эту кнопку.

Листинг 6.5. Содержимое файла price-quoter.ts

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

import {Stock} from './stock';

 

@Component({

    selector: 'price-quoter',

    template: `<strong><input type="button" value="Buy"

      (click)="buyStocks($event)">

        {{stockSymbol}} \${{lastPrice | currency:'USD':true:'1.2-2'}}

          </strong>

              `,

    styles:[`:host {background: pink; padding: 5px 15px 15px 15px;}`]

})

export class PriceQuoterComponent {

    @Output() buy: EventEmitter <Stock> = new EventEmitter();

    stockSymbol: string = "IBM";

    lastPrice:number;

    constructor() {

        setInterval(() => {

            this.lastPrice = 100*Math.random();

        }, 2000);

    }

    buyStocks(): void{

        let stockToBuy: Stock = {

            stockSymbol: this.stockSymbol,

            bidPrice: this.lastPrice

        };

        this.buy.emit(stockToBuy);

    }

}

Когда посредник (AppComponent) получает событие buy из элемента <price-quoter>, он извлекает из него полезную нагрузку и присваивает ее переменной stock, которая связана с входным параметром <order-processor>. Код показан далее (листинг 6.6).

Листинг 6.6. Содержимое файла mediator.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

import { BrowserModule } from '@angular/platform-browser';

import {OrderComponent} from './order';

import {PriceQuoterComponent} from './price-quoter';

import {Stock} from './stock';

 

@Component({

    selector: 'app',

    template: `

    <price-quoter (buy)="priceQuoteHandler($event)"></price-quoter><br>

    <br/>

    <order-processor [stock]="stock"></order-processor>

  `

})

class AppComponent {

    stock: Stock;

    priceQuoteHandler(event:Stock) {

        this.stock = event;

    }

}

 

@NgModule({

    imports:      [ BrowserModule],

    declarations: [ AppComponent, OrderComponent,

                    PriceQuoterComponent],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Когда значение входного свойства buy в компоненте OrderComponent изменяется, его сеттер отображает сообщение Placed order…, показывая stockSymbol и bidPrice (листинг 6.7).

Листинг 6.7. Содержимое файла order.ts

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

import {Stock} from './stock';

 

@Component({

    selector: 'order-processor',

    template: `{{message}}`,

    styles:[`:host {background: cyan;}`]

})

export class OrderComponent {

    message:string = "Waiting for the orders...";

    private _stock: Stock;

    @Input() set stock(value: Stock ){

        if (value && value.bidPrice != undefined) {

            this.message = `Placed order to buy 100 shares of

              ${value.stockSymbol} at \$${value.bidPrice.toFixed(2)}`;

        }

    }

    get stock(): Stock{

        return this._stock;

    }

}

Снимок экрана, показанный на рис. 6.5, был сделан после того, как пользователь нажал кнопку Buy (Купить) в момент, ко­гда цена на акции IBM составляла $12,17. Компонент PriceQuoteComponent отрисован в верхней части экрана, а OrderComponent — в нижней. Эти элементы самостоятельны и слабо связаны.

ch06_05.tif 

Рис. 6.5. Запуск примера с посредником

 

СОВЕТ

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

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

Альтернативная реализация шаблона «Посредник»

В этом разделе вы увидели, как компоненты одного уровня используют своих предков в качестве посредников. Если компоненты не имеют общего предка или не отображаются одновременно (маршрутизатор может не отображать требуемый элемент в данный момент), то можно применять в качестве посредника внедряемый сервис. При создании компонента в него внедряется сервис-посредник, и компонент может подписаться на события, отправляемые сервисом (в противоположность использованию параметров @Input(), как мы делали в компоненте OrderComponent).

Если вы хотите увидеть представленный подход в действии, то прочтите раздел «Предоставление результатов поиска компоненту HomeСomponent» упражнения в главе 8. Взгляните на код сервиса ProductService, играющий роль посредника. В этом сервисе определяется searchEvent: переменная EventEmitter используется компонентом SearchComponent для отправки данных, введенных пользователем. Компонент HomeComponent подписывается на переменную searchEvent для того, чтобы получать текст, который пользователь вводит в форме поиска.

6.1.3. Изменяем шаблоны во время работы программы с помощью ngContent

В некоторых случаях нужно динамически изменить содержимое компонента шаблона во время работы программы. В AngularJS эта функциональность называлась виртуальным включением, но теперь она известна как «проекция». В Angular можно спроецировать фрагмент шаблона компонента-предка на потомка с помощью директивы ngContent. Синтаксис довольно прост, вам нужно сделать всего два шага.

1. В шаблоне компонента-потомка включите теги <ng-content></ng-content> (точка внедрения).

2. В компоненте-предке включите фрагмент HTML, который вы хотите спроецировать на точку внедрения в потомке, между тегами, представляющими компонент-потомок (например, <my-child>):

template: `

    ...

    <my-child>

      <div>Passing this div to the child</div>

    </my-child>

    ...

`

В этом примере компонент-предок не будет отрисовывать содержимое, которое находится внутри тегов <my-child> и </my-child>. В листинге 6.8 показано применение данного приема.

Листинг 6.8. Содержимое файла basic-ng-content.ts

73665.png 

76524.png 

Мы также используем данный пример для демонстрации того, как работают Shadow DOM и ViewEncapsulation. Вы заметили, что и предок и потомок используют стиль .wrapper для внешнего элемента <div>? На обычной странице HTML это означало бы следующее: предок и потомок были бы отрисованы с применением одного стиля. Мы покажем такие способы инкапсуляции стилей компонентов-потомков, которые не будут конфликтовать со стилями предков, имеющими те же имена.

На рис. 6.6 показано запущенное приложение в режиме ViewEncapsulation.Native при открытой панели Developer Tools (Инструменты разработчика). Компонент ChildComponent получил содержимое HTML из компонента AppComponent и создал узлы Shadow DOM для предка и потомка (взгляните на элемент #shadow-root, расположенный справа). Обратите внимание на то, что стиль .wrapper из родительского элемента <div> (голубой фон) не был применен к элементу <div> потомка, также использующего стиль .wrapper, — он отрисовывается со светло-зеленым фоном. #shadow-root потомка выполняет роль стены, защищающей стили потомка, не давая наследовать стили предка.

73680.png 

Рис. 6.6. Запуск файла basic-ng-content.ts в режиме ViewEncapsulation.Native

Снимок экрана, показанный на рис. 6.7, был сделан после изменения режима инкапсуляции на ViewEncapsulation.Emulated. Структура DOM отличается от показанной ранее, и узлов #shadow-root больше нет. Angular сгенерировал дополнительные атрибуты для элементов предка и потомка, чтобы реализовать инкапсуляцию, но интерфейс отрисовывается точно так же.

73694.png 

Рис. 6.7. Запуск файла basic-ng-content.ts в режиме ViewEncapsulation.Emulated

На рис. 6.8 показан тот же пример, запущенный в режиме ViewEncapsula­tion.None. В этом случае все элементы предка и потомка объединяются в основное дерево DOM, а стили не инкапсулируются — для всего окна используется светло-зеленый фон предка.

ch06_08.tif 

Рис. 6.8. Запуск примера basic-ng-content.ts в режиме ViewEncapsulation.None

Проецирование на несколько областей. Компонент может иметь в своем шаблоне больше одного тега <ng-content>. Рассмотрим пример, когда шаблон потомка разбит на три области: верхний и нижний колонтитулы, а также зону содержимого. Разметка HTML для колонтитулов может быть спроецирована предком, а область содержимого может быть определена в потомке. Чтобы это реализовать, нужно включить в элемент-потомок две отдельные пары тегов <ng-content></ng-content>, заполняемые предком (колонтитулы).

Для гарантии того, что содержимое колонтитулов будет отрисовано в соответствующих областях <ng-content>, будет использоваться атрибут select, который может быть любым корректным селектором (класс CSS, имя тега и т.д.). Шаблон потомка может выглядеть так:

<ng-content select=".header"></ng-content>

<div>This content is defined in child</div>

<ng-content select=".footer"></ng-content>

Содержимое, поступающее от предка, будет соотнесено с помощью селектора и отрисовано в соответствующей области. Рассмотрим код, в котором полностью реализована эта функциональность (листинг 6.9).

Листинг 6.9. Содержимое файла ng-content-selector.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

import { BrowserModule } from '@angular/platform-browser';

 

@Component({

  selector: 'child',

  styles: ['.child {background: lightgreen;}'],

  template: `

    <div class="child">

    <h2>Child</h2>

      <ng-content select=".header" ></ng-content>

      <div>This content is defined in child</div>

      <ng-content select=".footer"></ng-content>

    </div>

  `

})

class ChildComponent {}

 

@Component({

  selector: 'app',

  styles: ['.app {background: cyan;}'],

  template: `

    <div class="app">

    <h2>Parent</h2>

      <div>This div is defined in the Parent's template</div>

      <child>

        <div class="header" >Child got this header from parent {{todaysDate}}

          </div>

        <div class="footer">Child got this footer from parent</div>

      </child>

    </div>

  `

})

class AppComponent {

  todaysDate: string = new Date().toLocaleDateString();

}

 

@NgModule({

  imports:      [ BrowserModule],

  declarations: [ AppComponent, ChildComponent],

  bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

ch06_09.tif 

Рис. 6.9. Запуск файла ng-content-select.ts

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

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

Прямая привязка к innerHTML

Вы можете связать свойство компонента, имеющее содержимое HTML, непосредственно с шаблоном, как показано в примере ниже:

<p [innerHTML]="myComponentProperty"></p>

Но вместо этого предпочтительнее использовать директиву ngContent по следующим причинам:

innerHTML — API, характерный для некоторых браузеров, а указанная директива не зависит от платформы;

с помощью данной директивы можно определить несколько мест, куда будут добавлены фрагменты HTML;

директива ngContent позволяет связать свойства компонента-предка и спроецированный HTML.

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

За время жизненного цикла с компонентом Angular случаются разные события. После создания элемента механизм определения изменений (он объясняется в следующем разделе) начинает за ним наблюдать. Компонент инициализируется, добавляется в модель DOM и отрисовывается, чтобы пользователь мог увидеть его. Затем состояние элемента (значения его свойств) может измениться, что вызовет перерисовку интерфейса; и наконец, компонент уничтожается.

На рис. 6.10 показаны привязки жизненного цикла (функции обратного вызова), где вы добавляете пользовательский код, если нужно. Функции обратного вызова, показанные на светло-сером фоне, будут вызваны всего один раз, а функции на темно-сером фоне — несколько раз.

73729.png 

Рис. 6.10. Жизненный цикл компонента

Пользователь видит компонент после завершения фазы инициализации. Затем механизм определения изменения гарантирует, что свойства элемента будут синхронизированы с его интерфейсом. Если компонент удален из дерева DOM в результате навигации или работы структурной директивы (например, ngIf), то Angular инициирует фазу разрушения.

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

ngOnChanges() — вызывается, когда компонент-предок изменяет (или инициализирует) значения, связанные с входными свойствами потомка. При отсутствии у компонента таких свойств данная функция не вызывается. Если вы хотите реализовать собственный алгоритм определения изменений, то его нужно разместить в функции DoCheck(). Но реализация этого алгоритма может оказаться дорогой, поскольку указанная функция вызывается после каждого цикла определения изменений.

• ngOnInit() — вызывается после первого вызова функции ngOnChanges(), если таковые случаются. Несмотря на то что вы можете реализовать некоторые переменные компонента в конструкторе, свойства компонента еще не будут готовы. Однако к моменту вызова ngOnInit() они уже будут инициализированы.

• ngAfterContentInit() — вызывается, когда инициализируется состояние компонента-потомка, если вы использовали директиву ngContent, чтобы передать ей какой-то код HTML.

• ngAfterContentChecked() — вызывается для компонента-потомка, который применил директиву ngContent после того, как получил содержимое от предка (или во время фазы определения изменений), если в директиве ngContent были использованы привязки.

• ngAfterViewInit() — вызывается, когда завершается привязка для шаблона компонента. Сначала инициализируется родительский элемент, а затем, если у него есть потомки, эта функция вызывается после того, как все они будут готовы.

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

Слово Content в имени метода обратного вызова жизненного цикла означает, что метод применяется, если содержимое проецируется с помощью директивы <ng-content>. Слово View в имени метода обратного вызова свидетельствует о его применении к шаблону компонента. Слово Checked означает, что изменения элемента применены и компонент синхронизируется с моделью DOM.

Для некоторых приложений может понадобиться вызывать определенную бизнес-логику, когда изменяется значение свойства. Например, финансовым приложениям может понадобиться записывать в журнал каждый шаг трейдера. Поэтому, если последний размещает заказ на покупку акций на сумму $101, а затем меняет сумму на $100, то данное изменение нужно записать в файл журнала. Приведенный пример хорошо показывает возможность использования журналирования в методе DoCheck().

Когда не нужно писать код в конструкторах

В приложении онлайн-аукциона вы внедряете сервис ProductService в конструктор компонента HomeComponent и вызываете там метод getProducts(). Если методу getProducts() нужно использовать значения свойств элемента, то переместите вызов этого метода в метод ngOnInit() для гарантии того, что все свойства были инициализированы к моменту вызова getProducts(). Переместить код из конструктора в данный метод можно и по другой причине: это позволит оставить код конструктора легким, не размещая в нем никаких долгоиграющих синхронных функций.

На этапе разрушения объекта приложение может очищать системные ресурсы. Предположим, компонент подписан на сервис уровня приложения, который отслеживает состояние программы (например, магазин приложений, предлагаемый библиотекой Redux). Когда Angular уничтожает данный компонент, он должен отписаться от этого сервиса с помощью функции обратного вызова ngOnDestroy().

примечание

Каждая функция обратного вызова жизненного цикла объявляется в интерфейсе с именем, соответствующим имени функции обратного вызова, но не имеет префикса ng. Например, если вы планируете реализовать какую-то функциональность в функции обратного вызова ngOnChanges(), то добавьте в объявление вашего класса конструкцию implements OnChanges.

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

6.2.1. Использование ngOnChanges

Проиллюстрируем привязки жизненного цикла компонента с помощью функции ngOnChanges(). В этом примере вы увидите элементы-предки и элементы-потомки, у последних будет два входных свойства: greeting и user. Первое свойство — строка, второе — объект с одним свойством, name. Чтобы понять, почему вызывается (или не вызывается) функция обратного вызова ngOnChanges(), нужно познакомиться с концепцией изменяемых и неизменяемых объектов.

Изменяемое против неизменяемого

Строки в JavaScript неизменяемы. Это значит, что созданную в памяти строку нельзя изменить. Рассмотрим следующий фрагмент кода:

var greeting = "Hello";

greeting = "Hello Mary";

В первой строке в определенной точке в памяти (а именно — по адресу @287651) создается значение Hello. Во второй строке значение по этому адресу не изменяется, поскольку создается новая строка Hello Mary в другом месте, а именно — по адресу @286777. Теперь в памяти содержится две строки, каждую из которых нельзя изменить.

Что происходит с переменной greeting? Ее значение изменяется, поскольку изначально она указывала на одну точку в памяти, а затем стала указывать на другую.

Объекты в JavaScript являются изменяемыми — это значит, что после создания объект остается в определенной точке в памяти, даже если изменяются значения их свойств. Рассмотрим следующий код:

var user = {name: "John"};

user.name = "Mary";

После выполнения первой строки кода создается объект, и переменная user указывает на определенную точку в памяти, @277500. Строка John создается в другой точке памяти, @287600, и переменная user.name сохраняет ссылку на этот адрес.

После выполнения второй строки кода создается новая строка Mary в третьей точке памяти, @287700, и переменная user.name сохраняет ссылку на этот новый адрес. Но переменная user все еще хранит адрес @277500. Другими словами, вы изменили содержимое объекта по адресу @277500.

Добавим в компонент-потомок привязку ngOnChanges(), чтобы показать, как она перехватывает изменения входных свойств. Это приложение имеет элементы-предки и элементы-потомки. Потомок имеет два входных свойства (greetings и user) и одно обычное (message). Пользователь может изменить значения входных свойств потомка. Покажем, какие значения свойств будут переданы методу ngOnChanges() в случае его вызова (листинг 6.10).

Листинг 6.10. Содержимое файла ng-onchanges-with-param.ts

73739.png 

76574.png 

Когда Angular вызывает метод ngOnChanges(), он передает туда значения каждого изменившегося свойства. Значение представлено объектом типа SimpleChange, который содержит текущее и предыдущее значение изменившегося входного свойства. Метод Simple.change.isFirstChange() позволяет определить, было ли значение свойства изменено, или же свойство получило значение в первый раз. Чтобы аккуратно вывести эти значения, вы используете метод JSON.stringify().

примечание

TypeScript имеет структурную систему типов, поэтому тип аргумента changes метода ngOnChanges() определяется путем включения описания ожида­емых данных. В качестве альтернативы можете объявить интерфейс (например, IChanges {[key: string]: SimpleChange};), и сигнатура функции будет выглядеть так: ngOnChanges(changes: IChanges). Предыдущее объявления свойства user компонента AppComponent — еще один пример использования структурного типа.

Взглянем, приведет ли изменение свойств greeting и user.name в интерфейсе к вызову метода ngOnChanges() компонента-потомка. На рис. 6.11 показан снимок экрана, сделанный после запуска листинга 6.10 с открытой панелью Developer Tools (Инструменты разработчика) в браузере Chrome.

Изначально, когда приложение применило привязку для входных свойств компонента-потомка, они не имели значений. Затем был вызван метод обратного вызова ngOnChanges() и предыдущие значения свойств greeting и user изменились с {} на Hello и {name:"John"} соответственно.

Включение режима коммерческой сборки

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

Для включения режима коммерческой сборки (prod-конфигурации) вызовите метод enableProdMode() в вашем приложении до вызова метода bootstrap(). Включение такого режима повысит производительность приложения в браузере.

ch06_11.tif 

Рис. 6.11. Изначальный вызов ngOnChanges()

Позволим пользователю изменить значения во всех полях ввода. После добавления слова dear в поле Greeting и переключения фокуса механизм определения изменений Angular обновляет привязку к неизменяемому входному свойству потомка, greeting. Затем вызывает функцию обратного вызова ngOnChanges и выводит предыдущее значение Hello и текущее значение Hello dear, как показано на рис. 6.12.

ch06_12.tif 

Рис. 6.12. Вызов метода ngOnChanges() после того, как изменилось значение свойства greeting

Теперь предположим, что пользователь добавляет в поле User Name (Имя пользователя) слово Smith и убирает оттуда фокус: в консоли не появится новых сообщений, как показано на рис. 6.13, поскольку изменилось только свойство name изменяемого объекта user; ссылка на сам объект не изменится. Это объясняет, почему не был вызван метод ngOnChanges(). Изменение значения свойства message компонента ChildComponent также не вызывает метод ngOnChanges(), поскольку данное свойство не было декорировано аннотацией @Input.

ch06_13.tif 

Рис. 6.13. Метод ngOnChanges() в этот раз не вызывается

примечание

Несмотря на то что Angular не обновляет привязки для входных свойств, если ссылка на объект не изменилась, механизм обнаружения изменений все еще отлавливает обновления свойств для каждого свойства объекта. Именно поэтому было отрисовано словосочетание John Smith, которое является новым значением поля User Name (Имя пользователя) в компоненте-потомке.

Ранее, в подразделе 6.1.1, вы использовали сеттер, чтобы определить момент, когда изменяется значение параметра ввода. Вместо этого можно задействовать метод ngOnChanges(). Существуют ситуации, когда вместо метода ngOnChanges() все же лучше применять сеттеры; вы познакомитесь с ними в разделе «Практикум» данной главы.

6.3. Краткий обзор определения изменений

Механизм определения изменений (change-detection, CD) в Angular реализован в файле zone.js (также известном как the Zone). Его основное предназначение заключается в том, чтобы синхронизировать значение свойств компонента (модели) и интерфейса. CD инициируется любым асинхронным событием, которое происходит в браузере (пользователь нажал кнопку, данные пришли с сервера, сценарий вызвал функцию setTimeout() и т.д.).

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

примечание

Механизм CD переносит изменения, сделанные в свойстве компонента, в интер­фейс. CD никогда не изменяет значение свойства компонента.

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

В Angular реализованы две стратегии CD: Default и OnPush. Если все компоненты используют первую стратегию, то Zone проверяет все дерево элементов независимо от того, где произошло изменение. Если какой-то элемент реализует вторую стратегию, то Zone проверяет компонент и его потомков только в случае изменений привязок к его входным свойствам. Чтобы объявить об использовании стратегии OnPush, нужно всего лишь добавить в шаблон компонента следующую строку:

changeDetection: ChangeDetectionStrategy.OnPush

Познакомимся с этими стратегиями с помощью трех компонентов: предка, потомка и «внука», они показаны на рис. 6.14.

73782.png 

Рис. 6.14. Стратегии определения изменений

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

С правой стороны рис. 6.14 показана ситуация, когда для потомка применяется стратегия OnPush. Механизм CD начинает свою работу с вершины, но видит, что для потомка объявлена стратегия OnPush. Если никакие привязки к входным свойствам данного элемента не изменились, CD не проверяет ни потомка, ни «внука».

На рис. 6.14 показано небольшое приложение, содержащее всего три компонента, но в реальных приложениях могут присутствовать сотни элементов. С помощью стратегии OnPush можно отказаться от вызова механизма CD для определенных ветвей дерева.

На рис. 6.15 показан цикл CD, который запустило событие компонента GrandChild1. Даже несмотря на то, что это событие произошло в листовом элементе, цикл CD начинает свою работу с вершины. Он выполняется для каждой ветви кроме тех, что происходят из компонента, для которого реализована стратегия OnPush и не изменились привязки его входных свойств. Компоненты, исключенные из цикла работы CD, показаны на белом фоне.

73793.png 

Рис. 6.15. Исключение ветки из цикла CD

Мы кратко рассмотрели механизм CD, который, возможно, является самым сложным модулем Angular. Изучать его более подробно следует только в том случае, если нужно повысить производительность приложения, активно обновляющего интерфейс, например, таблицы, содержащей сотни клеток, чьи значения постоянно изменяются. Для получения более подробной информации об определении изменений в Angular прочтите статью Виктора Савкина (Victor Savkin), которая называется Change Detection in Angular (Определение изменений в Angular) и представлена на .

6.4. Открываем доступ к API компонента-потомка

Вы узнали, как предок может передавать данные своим потомкам с помощью привязок к входным свойствам. Но существуют и другие ситуации, когда предку нужно использовать API, предоставляемый потомком. Мы покажем пример того, как элемент-предок может применять API потомка как для шаблона, так и для кода TypeScript.

Создадим простое приложение, в котором компонент-потомок будет иметь метод greet(), вызываемый предком. Чтобы показать разные приемы, предок будет задействовать два экземпляра одного потомка. Эти экземпляры будут иметь разные имена переменных шаблона:

<child #child1></child>

<child #child2></child>

Теперь можно объявить переменную в коде TypeScript, имеющем аннотацию @ViewChild.

Эта аннотация предоставляется Angular для того, чтобы помочь предоставить ссылку на компонент-потомок, и вы будете использовать ее для первого потомка:

@ViewChild('child1')

firstChild: ChildComponent;

...

this.firstChild.greet('Child 1');

Приведенный код указывает Angular найти компонент-потомок, определяемый переменной шаблона child1, и поместить ссылку на данный элемент в переменную firstChild.

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

<button (click)="child2.greet('Child 2')">Invoke greet() on child 2</button>

Далее показан весь код, иллюстрирующий оба приема (листинг 6.11).

Листинг 6.11. Содержимое файла exposing-child-api.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { NgModule, Component, ViewChild, AfterViewInit } from

  '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

 

@Component({

    selector: 'child',

    template: `<h3>Child</h3>`

})

class ChildComponent {

    greet(name) {

        console.log(`Hello from ${name}.`);

    }

}

 

@Component({

    selector: 'app',

    template: `

    <h1>Parent</h1>

    <child #child1></child>

    <child #child2></child>

    <button (click)="child2.greet('Child 2')">Invoke greet() on child 2

      </button>

  `

})

class AppComponent implements AfterViewInit {

    @ViewChild('child1')

    firstChild: ChildComponent;

    ngAfterViewInit() {

        this.firstChild.greet('Child 1');

    }

}

 

@NgModule({

    imports:      [ BrowserModule],

    declarations: [ AppComponent, ChildComponent],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Когда вы запускаете это приложение, оно выводит в консоли сообщение Hello from Child 1. Нажмите кнопку, и приложение выведет сообщение Hello from Child 2., как показано на рис. 6.16.

ch06_16.tif 

Рис. 6.16. Получение доступа к API потомка

Обновляем интерфейс с помощью привязок жизненного цикла

В листинге 6.11 используется привязка жизненного цикла ngAfterViewInit() для вызова API потомка. Если метод потомка greet() не изменяет интерфейс, то данный код отработает как полагается. Однако при попытке изменить интерфейс из метода greet() Angular сгенерирует исключение, поскольку интерфейс изменился после вызова метода ngAfterViewInit(). Это происходит потому, что данная привязка вызывается в рамках того же цикла событий как для предка, так и для потомка.

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

6.5. Практикум: добавление в онлайн-аукцион функциональности для оценивания товаров

В этом разделе вы добавите в приложение-аукцион функциональность для оценивания товаров. В предыдущей версии приложения вы просто отображали рейтинг товара, но теперь хотите дать пользователям возможность оценить продукт. В главе 4 вы создали представление Produce Details (Информация о продукте); здесь же добавите кнопку Leave a Review (Оставить отзыв). Она позволяет пользователям перемещаться к представлению, где они могут присвоить продукту от одной до пяти звезд, а также оставить отзыв. Фрагмент нового представления Produce Details (Информация о продукте) показан на рис. 6.17.

Компонент StarsComponent будет иметь входное свойство, которое будет изменяться. Только что добавленное значение рейтинга нужно передать его предку, ProductItemComponent.

примечание

Мы возьмем за основу приложение-аукцион, разработанное в главе 5. Если вы предпочитаете увидеть готовый результат этого проекта, то взгляните на исходный код главы 6, расположенный в каталоге auction. В противном случае скопируйте каталог auction из главы 5 в отдельное место. Затем скопируйте файл package.json из каталога auction для главы 6, запустите команду npm install и следуйте инструкциям, предоставленным в данном разделе.

Сделайте следующее.

1. Установите файл определения типа для прокладки ES6. Для этого запустите командную строку и выполните следующую команду:

npm install @types/es6-shim --save-dev

ch06_17.tif 

Рис. 6.17. Представление Produce Details (Информация о продукте)

Она установит файл ES6-shim.d.ts в каталог node_modules/@types и сохранит эту конфигурацию в разделе devDependencies файла package.json file. Вам необходимо использовать TypeScript 2.0, он знает, как искать файлы с определениями типов в каталоге @types.

2. Измените код файла StarsComponent. Новая версия должна работать в двух режимах: «только чтение» для отображения звезд на основе данных, полученных от сервиса ProductService, и «открыт к записи», чтобы позволить пользователям нажимать звезды для установки нового значения рейтинга.

На рис. 6.17 показана отрисовка компонента ProductDetailComponent (предок), где компонент StarsComponent (потомок) находится в режиме «только чтение». Если пользователь нажимает кнопку Leave a Review (Оставить отзыв), то данный режим нужно отключить. Вы добавите входную переменную readonly для включения и отключения этого режима.

Вторая входная переменная, rating, предназначена для присвоения рейтинга. Кроме того, вы добавите одну выходную переменную, ratingChange, которая будет отправлять событие, содержащее новую оценку; оно будет использоваться компонентом-предком для пересчета среднего рейтинга.

Когда пользователь нажмет одну из звезд, будет вызван метод fillStarsWithCo­lor(), который присвоит значение переменной rating и отправит ее значение с помощью события. Измените код файла stars.ts, чтобы он выглядел так, как показано в листинге 6.12.

Листинг 6.12. Обновленный файл stars.ts

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

 

  @Component({

    selector: 'auction-stars',

    styles: [`.starrating { color: #d17581; }`],

    templateUrl: 'app/components/stars/stars.html'

  })

  export default class StarsComponent {

    private _rating: number;

    private stars: boolean[];

    private maxStars: number =5;

    @Input() readonly: boolean = true;

    @Input() get rating(): number {

      return this._rating;

    }

 

  set rating(value: number) {

    this._rating = value || 0;

    this.stars = Array(this.maxStars).fill(true, 0, this.rating);

  }

 

  @Output() ratingChange: EventEmitter<number> = new EventEmitter();

 

  fillStarsWithColor(index) {

    if (!this.readonly) {

      this.rating = index + 1;

      this.ratingChange.emit(this.rating);

    }

  }

}

Вы задействуете сеттер для установки значения рейтинга. Этот сеттер может быть вызван либо из компонента StarsComponent (для отрисовки существующего рейтинга), либо из его предка (когда пользователь нажимает звезду). В данном приложении использование ngOnChanges() не сработает, поскольку он будет вызван предком всего раз при создании компонента StarsComponent.

Обратите внимание на применение метода fill() из ES6 в сеттере rating(). Вы заполняете массив stars значениями true, начиная с нулевого элемента и заканчивая элементом с номером, совпадающим со значением рейтинга. Чтобы заполнить звезду цветом, сохраняете значение true; для пустых звезд сохраняете false.

3. Измените шаблон компонента StarsComponent в файле stars.html, как это показано в листинге 6.13. С помощью директивы ngFor вы проходите в цикле по массиву stars, в котором хранятся булевы значения. Для пустых и закрашенных звезд вы будете использовать готовые изображения, поставляемые с библио­текой Bootstrap (см. /). В зависимости от значения элемента массива вы будете отрисовывать либо закрашенную, либо пустую звезду. Когда пользователь нажимает на звезду, вы передаете ее индекс функции fillStarsWithColor().

Листинг 6.13. Переработанный файл stars.html

<p>

  <span *ngFor="let star of stars; let i = index"

        class="starrating glyphicon glyphicon-star"

        [class.glyphicon-star-empty]="!star"

        (click)="fillStarsWithColor(i)">

  </span>

  <span *ngIf="rating">{{rating | number:'.0-2'}} stars</span>

</p>

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

4. Измените шаблон компонента ProductDetailComponent. Данный элемент имеет кнопку Leave a Review (Оставить отзыв), позволяющую оценить продукт и оставить отзыв о нем. Ее нажатие изменит видимость элемента <div>, который дает пользователям возможность нажимать на звезды и оставлять отзыв, как показано на рис. 6.18.

ch06_18.tif 

Рис. 6.18. Представление Leave a Review (Оставить отзыв)

Здесь компонент StarsComponent можно редактировать, и пользователь может дать выбранному продукту до пяти звезд. Шаблон, реализующий представление, показанное на рис. 6.18, будет выглядеть так:

<div [hidden]="isReviewHidden">

    <div><auction-stars [(rating)]="newRating"

      [readonly]="false" class="large"></auction-stars></div>

    <div><textarea [(ngModel)]="newComment"></textarea></div>

    <div><button (click)="addReview()" class="btn">Add review

      </button></div>

</div>

Режим «только чтение» отключен. Обратите внимание на то, что в двух местах используется двухсторонняя привязка: [(rating)] и [(ngModel)]. Ранее в данной главе вы рассматривали применение директивы ngModel в двухсторонней привязке, но если у вас есть входное свойство (например, rating) и выходное, имеющее такое же имя и суффикс Change (например, ratingChange), то можете задействовать для них синтаксис [()].

Кнопка Leave a Review (Оставить отзыв) изменяет видимость элемента <div>. Это реализуется так:

<button (click)="isReviewHidden = !isReviewHidden"

                class="btn btn-success btn-green">Leave a Review</button>

Замените содержимое файла product-detail.html на приведенное ниже (листинг 6.14).

Листинг 6.14. Измененный файл product-detail.html

<div class="thumbnail">

    <img src="">

    <div>

        <h4 class="pull-right">{{ product.price }}</h4>

        <h4>{{ product.title }}</h4>

        <p>{{ product.description }}</p>

    </div>

    <div class="ratings">

        <p class="pull-right">{{ reviews.length }} reviews</p>

        <p><auction-stars [rating]="product.rating" ></auction-stars></p>

    </div>

</div>

 

<div class="well" id="reviews-anchor">

    <div class="row">

        <div class="col-md-12"></div>

    </div>

    <div class="text-right">

        <button (click)="isReviewHidden = !isReviewHidden"

                class="btn btn-success btn-green">Leave a Review</button>

    </div>

 

    <div [hidden]="isReviewHidden">

        <div><auction-stars [(rating)]="newRating"

          [readonly]="false" class="large"></auction-stars></div>

        <div><textarea [(ngModel)]="newComment"></textarea></div>

        <div><button (click)="addReview()" class="btn">Add review</button>

          </div>

    </div>

 

    <div class="row" *ngFor="#review of reviews">

        <hr>

        <div class="col-md-12">

            <auction-stars [rating]="review.rating"></auction-stars>

            <span>{{ review.user }}</span>

            <span class="pull-

              right">{{ review.timestamp | date: 'shortDate' }}</span>

            <p>{{ review.comment }}</p>

        </div>

    </div>

</div>

После ввода текста отзыва и оценки продукта пользователь нажимает кнопку Add Review (Добавить отзыв), которая вызывает для компонента метод addReview(). Реализуем его с помощью TypeScript.

5. Измените файл product-detail.ts. Для добавления отзыва нужно делать следующее: отправить новый отзыв на сервер и пересчитать средний рейтинг продукта в интерфейсе. Вы проделаете последнее действие, еще не реализовав коммуникацию с сервером; отзыв мы будем размещать в консоли браузера. Далее вы добавите новый отзыв в массив существующих отзывов. В следующем фрагменте кода компонента ProductDetailComponent реализуется эта функциональность:

addReview() {

  let review = new Review(0, this.product.id, new Date(), 'Anonymous',

      this.newRating, this.newComment);

  console.log("Adding review " + JSON.stringify(review));

  this.reviews = [...this.reviews, review];

 

  this.product.rating = this.averageRating(this.reviews);

  this.resetForm();

}

 

averageRating(reviews: Review[]) {

  let sum = reviews.reduce((average, review) => average + review.rating, 0);

  return sum / reviews.length;

}

После создания нового экземпляра объекта класса Review вам нужно добавить его в массив reviews. Оператор расширения позволяет записать его элегантным образом:

this.reviews = [...this.reviews, review];

Массив reviews получает значения всех существующих элементов (…this.re­views), а также нового (review). Пересчитаннное среднее значение присваивает­ся свойству rating, значение которого попадает в интерфейс с помощью привязки.

Что нам осталось сделать? Заменим содержимое файла product-detail.ts на следующий код (листинг 6.15), и это упражнение закончится!

Листинг 6.15. Измененный файл product-detail.ts

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

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

import {Product, Review, ProductService} from

  '../../services/product-service';

import StarsComponent from '../stars/stars';

 

4@Component({

  selector: 'auction-product-page',

  styles: ['auction-stars.large {font-size: 24px;}'],

  templateUrl: 'app/components/product-detail/product-detail.html'

})

 

export default class ProductDetailComponent {

  product: Product;

  reviews: Review[];

 

  newComment: string;

  newRating: number;

 

  isReviewHidden: boolean = true;

 

  constructor(route: ActivatedRoute, productService: ProductService) {

 

    let prodId: number = parseInt(route.snapshot.params['productId']);

    this.product = productService.getProductById(prodId);

    this.reviews = productService.getReviewsForProduct(this.product.id);

  }

 

  addReview() {

    let review = new Review(0, this.product.id, new Date(), 'Anonymous',

        this.newRating, this.newComment);

    console.log("Adding review " + JSON.stringify(review));

    this.reviews = [...this.reviews, review];

    this.product.rating = this.averageRating(this.reviews);

 

    this.resetForm();

  }

 

  averageRating(reviews: Review[]) {

    let sum = reviews.reduce((average, review) => average + review.rating,0);

    return sum / reviews.length;

  }

 

  resetForm() {

    this.newRating = 0;

    this.newComment = null;

    this.isReviewHidden = true;

  }

}

6.6. Резюме

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

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

Вот основные выводы этой главы.

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

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

• Коммуникация между несвязанными компонентами может быть выстроена с помощью шаблона проектирования «Посредник».

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

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

• Механизм определения изменений Angular автоматически отслеживает изменения свойств компонента и соответствующим образом обновляет интерфейс.

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

Назад: 5. Привязки, наблюдаемые объекты и каналы
Дальше: 7. Работа с формами

32
32
Alex
32
fagmefs
kamagra 100 mg on line
lasix other names
Amoxicillin 250 5ml
dragzolotoru
Ювелирные изделия, те что покупатели имеем или приобретаем именно на подарок родным несут внутри себе большое число увлекательных данных, которые реально просто изучить, когда Вы нажмет на источник новых публикаций касательно драгоценных изделий виды плетение цепочек. Интернет страничка золотых украшений ознакомит любителей из спектр полезными умений, с пособием их Вы могут хорошо ориентироваться у высококачества металлических плюс популярных изделий, к тому же дорогих породах камней плюс металлах. Онлайн сайт всякой проверенных данных о украшения ежедневно увеличивает сведения, те что смогут Вам вернее разбираться какие именно изделия совмещаются в стиле, как же требуется чистить про ювелирными предметами, и что сегодня актуально. Переходите на сайт, читайте также лайкайте подходящие вам новости или же делитесь ссылки на странички соц. сети, здесь на сайте мы будем систематически дополнять существующие библиотеку постов ювелирных украшений.
CharlesTut
импорт товаров
JacobBal
Каждый человек способен испытать испуг в разных жизненных ситуациях. Это – абсолютно обычное явление, помогающее нам спасти себе жизнь в момент угрозы жизни. Правда большинство страхов это просто напросто иррациональное переживание. Например страх летать на самолёте. С этими страхами возможно будет бороться успешно, существуют очень действенные технологии, про них узнаете в анализе ссылка на статью Также можно почувствовать страх опасности, именно это ощущение помогает спасти человеческую жизнь в угрожающих ситуациях. Соответственно страх это адекватное состояние, проблема может возникнуть только лишь если подобный страх часто сводит с ума. В этом случае надо предпринимать определенные меры, обращаться в психологический центр.
Caseybup
електро тепла підлога
Howardton
электро полы с подогревом