Книга: Angular и TypeScript. Сайтостроение для профессионалов
Назад: 4. Внедрение зависимостей
Дальше: 6. Реализация коммуникации между компонентами

5. Привязки, наблюдаемые объекты и каналы

В этой главе:

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

• привязки к атрибутам против привязок к свойствам;

• исследуем наблюдаемые потоки данных;

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

• минимизируем нагрузку на сеть путем отмены нежелательных запросов HTTP;

• минимизируем объем кода с помощью каналов.

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

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

5.1. Привязка данных

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

73364.png 

В Angular привязка данных является односторонней. Это означает либо отображение изменений данных в свойствах элемента в интерфейсе, либо привязку событий пользовательского интерфейса к методам компонента. Например, при обновлении свойства компонента productTitle представление (шаблон) автоматически обновляется с помощью следующего синтаксиса в шаблоне: {{productTitle}}. Аналогично, когда пользователь что-то вводит в поле <input>, привязка событий (отмеченное скобками) вызывает обработчик событий с правой стороны знака «равно»:

(input) = "onInput()"

примечание

В шаблонах двойные фигурные скобки в тексте и квадратные скобки в атрибутах элементов HTML указывают на привязку свойств. Angular связывает интерполированное значение (строку, в которую были внедрены значения выражения) и свойство textContent соответствующего узла DOM. Это присваивание выполняется не один раз — текст постоянно обновляется по мере изменения соответствующего значения.

Что было не так с двухсторонней привязкой в AngularJS?

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

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

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

привязка событий для вызова функций, которые обрабатывают эти события;

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

• привязка свойств для обновления значений свойств элементов DOM;

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

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

5.1.1. Привязки к событиям

Чтобы назначить событию функцию-обработчик, нужно поместить имя события в круглых скобках в шаблон компонента. В следующем фрагменте кода показано, как привязать функцию onClickEvent() к событию click, а функцию onInputEvent() — к событию input:

<button (click)="onClickEvent()">Get Products</button>

<input placeholder="Product name" (input)="onInputEvent()">

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

Если нужно проанализировать свойства объекта события, то добавьте аргумент $event в функцию-обработчик. В частности, целевое свойство объекта события представляет собой узел DOM, в котором произошло событие. Объект события будет доступен только в определенной области видимости (то есть в функции — обработчике события). На рис. 5.1 показано, как читать синтаксис привязки событий.

73372.png 

Рис. 5.1. Синтаксис привязки событий

Событие, расположенное в круглых скобках, называется целью привязки. Можно привязывать функции к любым стандартным событиям DOM, существующим сегодня (см. раздел Event reference в документации Mozilla Developer Network, ), а также к тем, которые будут введены в будущем. Кроме того, можно создавать пользовательские события и привязывать к ним функции-обработчики точно таким же способом (см. «Выходные свойства и пользовательские события» в подразделе 6.1.1).

5.1.2. Привязка к свойствам и атрибутам

Каждый элемент HTML представлен тегом, имеющим атрибуты, и браузер создает объект модели DOM, который имеет свойства для каждого тега. Пользователь видит такие объекты на экране по мере того, как браузер их отрисовывает. Вы должны понимать, что находится в любой момент времени в трех конкретных областях, таких как:

документ HTML;

• объект модели DOM;

• отрисованный пользовательский интерфейс.

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

Свойства

Рассмотрим следующий тег <input>:

<input type="text" value="John" required>

Браузер использует эту строку для создания узла в дереве DOM, который является объектом JavaScript типа HTMLInputElement. Каждый объект модели DOM имеет API, состоящий из методов и свойств (см. раздел HTMLInputElement в документации Mozilla Developer Network, ). В частности, объект HTMLInputElement содержит свойства type и value типа DOMString, а также свойство required типа Boolean. Браузер отрисовывает данный узел DOM.

примечание

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

В Angular вы объявляете привязку свойства, поместив его имя в квадратные скобки и присвоив ему выражение (или переменную класса). На рис. 5.2 показано, как работает механизм привязки свойств в Angular. Представьте компонент MyComponent, который имеет класс, содержащий переменную greeting. Шаблон этого компонента содержит тег <input>, классовая переменная greeting привязана к свойству value.

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

Когда следует использовать привязку свойств

Привязку свойств целесообразно применять в таких сценариях:

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

компонент-предок должен обновить свойство своего потомка (см. пункт «Входные свойства» на с. 204).

73380.png 

Рис. 5.2. Привязка свойств

Атрибуты

Мы используем слово «атрибуты» в контексте документа HTML (но не объекта модели DOM). Привязка атрибутов применяется редко, поскольку браузер для сборки дерева DOM задействует HTML, после чего работает в основном со свойствами объекта модели DOM. Но существуют сценарии, в которых привязка атрибутов может понадобиться. Например, скрытые атрибуты не поддерживаются в браузере Internet Explorer 10, и он не создаст соответствующий атрибут модели DOM, поэтому, если нужно изменять видимость компонента с помощью стилей CSS, то привязка атрибутов поможет. Еще одним примером является интеграция с фреймворком Google Polymer — это можно сделать, только используя привязку атрибутов.

Как и в случае со свойствами, привязка атрибутов обозначается путем размещения имени атрибута в квадратных скобках. Но для оповещения Angular о том, что нужно привязать атрибут (а не свойство модели DOM), следует добавить префикс attr.:

<input [attr.value]="greeting">

На рис. 5.3 показана привязка атрибутов.

73398.png 

Рис. 5.3. Привязка атрибутов

Посмотрим, как работает привязка свойств и атрибутов, на одном примере. В листинге 5.1 показан элемент <input>, который использует привязку атрибута value и свойства value. Этот фрагмент кода располагается в файле attribute-vs-property.ts.

Листинг 5.1. Содержимое файла attribute-vs-property.ts

73407.png 

Если вы запустите данную программу и начнете набирать текст в поле ввода, то она выведет в консоли браузера содержимое значения value модели DOM, атрибута value элемента HTML <input>, а также содержимое свойства greeting компонента MyComponent. На рис. 5.4 показана консоль после запуска этой программы и ввода значения 3 в поле <input>.

Значение атрибута value не изменилось, как и свойства greeting; это доказывает, что в Angular не используется двухсторонняя привязка. В AngularJS изменение модели (свойства greeting) обновило бы представление, а если бы пользователь изменил данные в представлении, то модель бы автоматически обновилась.

ch05_04.tif 

Рис. 5.4. Запуск примера attribute-vs-property

Упрощение кода с помощью деструктурирования

В приложении А мы рассмотрим деструктурирование в рамках ES6, но оно также поддерживается и TypeScript. Деструктурирование может упростить код функции — обработчика событий onInputEvent(), показанной в листинге 5.1.

Данная функция получает объект Event, а затем с помощью одной строки вы извлекаете значение из целевого свойства. Синтаксис деструктурирования поможет избавиться от строки, извлекающей значение из свойства event.target:

onInputEvent({target}): void {

  console.log(`The input property value = ${target.value}`);

  console.log(`The input attribute value =

    ${target.getAttribute('value')}`);

  console.log(`The greeting property value = ${this.greeting}`);

}

Использование фигурных скобок в аргументе этой функции указывает ей следующее: «Вы получите объект, который имеет целевое свойство. Просто выдайте мне значение данного свойства».

5.1.3. Привязка в шаблонах

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

Angular предлагает использовать структурные директивы (NgIf, NgSwitch и NgFor), которые изменяют структуру модели DOM, добавляя или удаляя элементы из нее. Директива NgIf в зависимости от выполнения некоего условия может добавить элемент в дерево модели DOM или удалить его из дерева. Директива NgFor проходит по массиву и добавляет элементы в дерево DOM для каждого элемента массива. Директива NgSwitch добавляет один элемент в дерево модели DOM из набора возможных элементов в зависимости от выполнения определенного условия. С помощью привязки шаблонов можно дать Angular команду сделать это для вас. Лучше удалять элементы, чем скрывать их, если нужно гарантировать, что приложение не будет тратить время на поддержку поведения этих элементов (например, на обработку событий или наблюдение за изменениями).

Шаблоны HTML и директивы Angular

Тег HTML <template> (см. раздел <template> в документации к Mozilla Developer Network, ) — это необычный тег, поскольку браузер игнорирует его содержимое, если приложение не предоставит сценарий для его анализа и добавления в модель DOM. Angular предлагает так называемый сокращенный синтаксис для директив: они начинаются со звездочки, например *ngIf или *ngFor. Когда анализатор Angular видит директиву, чье имя начинается со звездочки, он преобразует ее во фрагмент HTML, который использует тег <template> и распознается браузерами.

В листинге 5.2 включен один элемент <span> и один элемент <template>, в нем проиллюстрированы два вида привязки шаблонов с помощью директивы NgIf. В зависимости от значения флага (которое изменяется после нажатия кнопки) элементы <span> будут добавлены в модель DOM либо убраны из нее.

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

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

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

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

 

@Component({

    selector: 'app',

    template: `

    <button (click)="flag = !flag">Toggle flag's value</button>

    <p>

      Flag's value: {{flag}}

    </p>

    <p>

      1. span with *ngIf="flag": <span *ngIf="flag">Flag is true</span>

    </p>

    <p>

      2. template with [ngIf]="flag": <template [ngIf]="flag">Flag is true

        </template>

    </p>

  `

})

class AppComponent {

    flag: boolean = true;

}

 

@NgModule({

    imports:      [ BrowserModule],

    declarations: [ AppComponent],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

В отличие от остальных видов привязки, доступных в Angular, привязка шаблона преобразует шаблон представления. Код, показанный в листинге 5.2, добавляет или удаляет значение флага из дерева DOM в зависимости от выполнения определенного условия. Вы будете использовать как сокращенный синтаксис, *ngIf="flag", чтобы обработать элемент <span>, так и полную версию, [ngIf]="flag", для обработки содержимого тега <template>.

На рис. 5.5 показано, что, когда флаг имеет значение true, дерево DOM включает содержимое элементов <span> и <template>. На рис. 5.6 видно, что, когда флаг имеет значение false, дерево DOM не содержит этих элементов.

73439.png 

Рис. 5.5. Привязка шаблонов: флаг имеет значение true

73450.png 

Рис. 5.6. Привязка шаблонов: флаг имеет значение false

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

5.1.4. Двухсторонняя привязка данных

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

Вы уже знаете, что односторонняя привязка элемента интерфейса и компонента Angular выполняется путем взятия имени события в скобки:

<input (input)="onInputEvent($event)">

Односторонняя привязка компонента к элементу интерфейса обозначается путем взятия атрибута HTML в квадратные скобки:

<input [value]="myComponentProperty" >

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

<input [value]="myComponentProperty"

(input)="onInputEvent($event)>

Angular также предлагает более короткую объединенную нотацию: [()]. В частности, фреймворк имеет директиву NgModel, которую можно применять для настройки двухсторонней привязки (обратите внимание: когда директива NgModel используется в шаблонах, ее имя пишется со строчной буквы):

<input [(ngModel)] = "myComponentProperty">

Вы все еще можете увидеть свойство myComponentProperty, но какое событие оно обрабатывает? В данном примере директива NgModel применяется вместе с элементом <input>. Это событие является триггером по умолчанию для синхронизации изменений, сделанных в пользовательском интерфейсе в элементе HTML <input>, с моделью. Но управляющее событие может быть и другим, в зависимости от того, используется ли элемент управления пользовательского интерфейса вместе с ngModel. Оно управляется изнутри специальным интерфейсом Angular ControlValueAccessor, который выступает в качестве моста между элементом управления и нативным элементом. Интерфейс ControlValueAccessor служит для создания пользовательских элементов управления.

Двухсторонняя привязка часто применяется для тех форм, где нужно синхронизировать значения полей форм и свойства модели форм. В главе 7 мы рассмотрим использование директивы NgModel более подробно. Вы узнаете, как работать с формами, не задействуя конструкцию [(ngModel)] для каждого элемента управления формы; но иногда возникают ситуации, когда это может пригодиться, так что познакомимся с данным синтаксисом.

Предположим, что посадочная страница финансового приложения позволяет пользователю проверять последние цены на акции путем ввода символов в специальное поле. Пользователи зачастую будут вводить символы тех акций, за которыми они следят или которыми обладают, например, AAPL для компании Apple. Можно сохранять последний введенный символ как cookie (или в локальном хранилище HTML5); когда пользователь в следующий раз откроет страницу, программа может считать cookie и заполнить поле ввода. Пользователь должен иметь возможность ввести и другие символы в это поле, и введенные значения должны синхронизироваться с переменной lastStockSymbol, играющей роль модели. В листинге 5.3 реализована описанная функциональность.

Листинг 5.3. Содержимое файла two-way-binding.ts

73459.png 

76380.png 

Переменная lastStockSymbol и значение поля <input> всегда будут синхронизированы. Вы можете увидеть этот код в действии, запустив сценарий файла two-way-binding.ts.

примечание

В листинге 5.3 для реализации двухсторонней привязки данных служит директива NgModel, но для этого можно задействовать и свойства, характерные для самого приложения. Нужно применять в именах этих свойств специальный суффикс, Change. В разделе 6.5 вы увидите, как можно изменять рейтинг продукта, используя двухстороннюю привязку с помощью синтаксиса [(rating)].

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

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

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

5.2. Реактивное программирование и наблюдаемые потоки

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

примечание

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

Реактивные расширения реализованы во многих библиотеках, которые поддерживают наблюдаемые потоки. Одной из таких библиотек является RxJS (). Она интегрирована в Angular.

5.2.1. Что такое «наблюдаемые потоки» и «наблюдатели»

Наблюдатель — объект, который обрабатывает поток данных, отправляемый наблюдаемой функцией. Существует два основных типа наблюдаемых потоков: горячие и холодные. Холодные наблюдаемые потоки начинают отправлять данные, только когда какой-то код вызывает функцию subscribe(). Горячие наблюдаемые потоки отправляют данные даже в том случае, если ни одному подписчику они не нужны. В этой книге мы будем использовать только холодные наблюдаемые потоки.

Сценарий, который подписывается на наблюдаемый поток, предоставляет объект-наблюдатель, знающий, что делать с элементами потока:

let mySubscription: Subscription = someObservable.subscribe(myObserver);

Чтобы отменить подписку на поток, вызовите метод unsubscribe():

mySubscription.unsubscribe();

Наблюдаемым потоком является объект, отправляющий элементы из некоторого источника данных (сокета, массива, событий интерфейса) по одному за раз. Говоря точнее, наблюдаемый поток знает, как делать три вещи:

отправить следующий элемент;

• сгенерировать ошибку;

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

Соответственно, объект-наблюдатель предоставляет до трех функций обратного вызова:

функцию для обработки следующего элемента, отправленного наблюдаемым потоком;

• функцию для обработки ошибок наблюдаемого потока;

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

примечание

В приложении А мы рассмотрим объект Promise, способный всего один раз вызывать обработчик событий, указанный в функции then(). Метод subscribe() похож на последовательность вызовов метода then(): по одному вызову на каждый прибывающий элемент данных.

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

73472.png 

Рис. 5.7. От наблюдаемого потока к подписчику

Более реалистичным примером будет поток объектов типа Customer, который становится другим потоком, содержащим только свойство age для каждого покупателя. Первый поток можно профильтровать так, чтобы оставить только покупателей, чей возраст меньше 50 лет.

примечание

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

В документации для реактивных расширений (см. раздел Operators в документации к ReactiveX, ) для иллюстрации операторов используются интерактивные схемы. Например, оператор map() представлен в виде интерактивной схемы, показанной на рис. 5.8.

ch05_08.tif 

Рис. 5.8. Интерактивная схема для метода map

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

СОВЕТ

Обратите внимание на сайт RxMarbles (), где можно найти интерактивные схемы для многих операторов Rx.

От массивов к итерабельным объектам и наблюдаемым потокам

JavaScript имеет несколько полезных методов для работы с массивами данных (представлены ниже).

Метод map() позволяет применять функцию к каждому элементу массива. С помощью map() можно преобразовать один массив в другой, не изменяя количество элементов. Например, вызов может выглядеть так: myArray.map(convertToJSON).

Метод filter() позволяет применять функцию к каждому элементу массива, отфильтровывая элементы на основе некоторой бизнес-логики. Например, вызов может выглядеть так: myArray.filter(priceIsLessThan100). Итоговый массив может содержать меньше элементов, чем оригинальный.

Метод reduce() помогает получать среднее значение элементов массива. Например, вызов может выглядеть так: myArray.reduce((x,y) => x+y). Результатом работы метода reduce() всегда будет одно значение.

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

Источником итерабельных данных не обязательно должен быть массив. Можно написать функцию-генератор с помощью ES6 (см. приложение А), которая возвращает ссылку на итератор, а затем начать получать данные (по одному элементу за раз) из этого итератора, используя следующую конструкцию: myIterator.next().value. Для каждого значения можно применить какую-нибудь бизнес-логику, а затем перейти к следующему элементу.

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

Возможно, концепцию наблюдаемых потоков будет проще понять, если визуализировать данные, поступающие с сервера. Вы увидите подобные примеры и далее в этой главе, и в главе 8, когда научитесь работать с запросами HTTP и WebSockets, но концепцию наблюдаемого потока можно применить и к событиям. Является ли событие одноразовым явлением, которому нужна лишь функция-обработчик? Можно ли рассматривать события как последовательность элементов, поступающую со временем? Далее мы рассмотрим потоки событий.

примечание

О том, как можно превратить любой сервис в наблюдаемый поток, написано в главе 8.

5.2.2. Наблюдаемые потоки событий

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

Каждое событие представлено объектом Event (или его потомком), содержащим свойства, описывающие событие. Angular-приложения могут обрабатывать стандартные события модели DOM, а также создавать и рассылать пользовательские события.

Функцию — обработчик события можно объявить с необязательным параметром $event, содержащим объект JavaScript, чьи свойства описывают событие. В случае стандартных событий модели DOM можно использовать любые функции или свойства объекта Event браузера (см. раздел Event в документации к Mozilla Developer Network, ).

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

template:`<input (keyup)="onKey($event)">`

...

onKey(event:any) {

  console.log("You have entered " + event.target.value);

}

В предыдущем фрагменте кода вы получаете доступ к свойству value элемента <input> путем использования свойства event.target, указывающего на элемент, отправленный событию. Но Angular позволяет получить элемент HTML (и его свойства) прямо в шаблоне, объявив локальную переменную шаблона, которая всегда будет содержать ссылку на свой элемент HTML.

В следующем фрагменте кода объявлена локальная переменная шаблона mySearchField (имя должно начинаться с символа решетки), а также извлекается значение размещающего ее элемента HTML (в нашем случае <input>), переда­ющееся функции — обработчику событий вместо ссылки на объект Event. Обратите внимание: символ решетки нужен только для того, чтобы объявить локальную переменную в шаблоне; вам не нужно использовать этот символ при работе с данной переменной в коде JavaScript:

template:`<input #mySearchField (keyup)="onKey(mySearchField.value)">`

...

onKey(value: string) {

  console.log("You have entered " + value);

}

примечание

Если ваш код отправляет пользовательское событие, то оно способно нести характерные для приложения данные, а объект события может быть строго типизирован (а не просто являться типом any). Вы увидите, как это делается, в главе 6 в пункте «Выходные свойства и пользовательские события» на с. 26.

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

Подписываясь на поток, ваш код выражает заинтересованность в получении его элементов. Во время подписки вы указываете код, который нужно вызвать при получении следующего элемента, а также — опционально — код для обработки ошибок и сигнала о завершении потока. Зачастую вы укажете несколько операторов, а затем вызовете метод subscribe().

Как можно применить это к событиям, поступающим от интерфейса? Например, использовать привязку событий, которая обрабатывает несколько событий keyup и значение lastStockSymbol:

<input type='text' (keyup) = "getStockPrice($event)">

Достаточно ли хороша данная техника для обработки нескольких событий? Представьте, что предыдущий код используется для получения коммерческого предложения на акции AAPL. После того как пользователь введет первый символ A, функция getStockPrice() создаст основанный на промисе запрос на сервер, который вернет цену на акции A, если таковые имеются. Далее пользователь вводит второй символ A, что приводит к отправке еще одного запроса на сервер, на этот раз для получения предложения с расценками для AA. Процесс повторяется для комбинаций AAP и AAPL.

Вы хотите другого, поэтому создаете задержку 500 миллисекунд, чтобы дать пользователю достаточно времени на ввод нескольких букв. Здесь поможет функция setTimeout()!

Что, если пользователь набирает текст медленно и за 500 миллисекунд успеет набрать только AAP? На сервер отправится первый запрос для данной комбинации, а спустя 500 миллисекунд будет отправлен второй запрос для комбинации AAPL. Программа не может отменить первый HTTP-запрос в случае возврата сервером объекта Promise, поэтому вам придется скрестить пальцы, понадеявшись, что пользователи будут набирать текст быстро и не перегрузят сервер ненужными запросами.

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

Напишем небольшое приложение, использующее простую форму с одним полем ввода, генерирующим наблюдаемый поток. Чтобы понять следующий пример, нужно знать, что элементы формы связаны со свойствами компонента Angular с помощью атрибута formControl.

примечание

Существует способ программирования форм с использованием директив, размещаемых в шаблоне компонента — они называются шаблон-ориентированными формами. Кроме того, можно программировать формы путем создания объектов, связанных с формами, в коде TypeScript ваших компонентов. Такие формы называются реактивными. Мы подробнее рассмотрим формы в главе 7.

В листинге 5.4 перед вызовом метода subscribe() применяется всего один оператор, debounceTime(). RxJS поддерживает десятки операторов, пригодных к использованию для наблюдаемых потоков (см. документацию к RxJS, ), но в Angular реализованы не все из них. Именно поэтому нужно импортировать дополнительные операторы из библиотеки RxJS, которая представляет собой зависимость Angular. Оператор debounceTime() позволяет указать задержку генерации элементов данных потока.

Листинг 5.4. Содержимое файла observable-events.ts

73491.png 

Метод subscribe() создает объект класса Observer, который в нашем случае передает каждое значение из потока, сгенерированное элементом searchInput, методу getStockQuoteFromServer(). В реальном сценарии этот метод создаст запрос на сервер, и вы увидите такое приложение в следующем разделе; сейчас же данная функция генерирует случайное число.

Если вы не использовали оператор debounceTime(), то событие valueChanges будет отправляться после каждого символа, введенного пользователем. Для предот­вращения обработки каждого нажатия клавиши вы даете элементу searchInput команду отправлять данные с задержкой 500 миллисекунд, что позволяет пользователю ввести несколько символов до того, как содержимое поля будет отправлено в поток. На рис. 5.9 показан снимок экрана, сделанный после того, как мы запустили приложение и ввели комбинацию символов AAPL в поле ввода.

ch05_09.tif 

Рис. 5.9. Получение цены для комбинации символов AAPL

СОВЕТ

Независимо от того, сколько операторов вы введете одновременно, ни один из них не будет вызван до тех пор, пока вы не вызовете метод subscribe().

примечание

В листинге 5.4 обрабатывается наблюдаемый поток, который предоставляется объектом FormControl, когда объект модели DOM отправляет событие change. Если вы предпочитаете генерировать наблюдаемый поток на основе другого события (например, keyup), то можете использовать API Observable.fromEvent() из библиотеки RxJS (см. документацию к RxJS на GitHub, ).

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

В листинге 5.4 не выполняются сетевые запросы на сервер для получения цен на акции — вы генерируете случайные числа на компьютере пользователя. Даже если пользователь введет неверную комбинацию символов, то это приведет к вызову метода Math.random(), что незначительно скажется на производительности приложения. В реальных приложениях опечатки, сделанные пользователем, могут сгенерировать сетевые запросы, вызывающие задержки из-за возврата сервером предложений для неверно введенных символов. В следующем разделе мы покажем, как отменять ожидающие серверные запросы для наблюдаемых потоков.

5.2.3. Отменяем наблюдаемые потоки

Одним из преимуществ наблюдаемых потоков перед промисами является тот факт, что их можно отменить. В предыдущем разделе мы показали сценарий, в котором опечатка может привести к созданию бесполезных запросов на сервер. Реализация представления Master-detail (Один ко многим) — еще один пример, когда может понадобиться отменить запрос. Предположим, пользователь нажимает строку в списке товаров для получения подробной информации о продукте, которую нужно получить с сервера. Затем он передумывает и нажимает другую строку, что приводит к созданию другого запроса; в этом случае ожидающий запрос в идеале должен быть отменен.

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

наблюдаемый поток, создаваемый полем поиска;

• наблюдаемый поток, создаваемый HTTP-запросами, отправленными, когда пользователь вводит данные в поле поиска.

В этом примере (файл observable-events-) вы будете использовать бесплатный погодный сервис , которая предоставляет API, позволяющий делать запросы о погоде в городах по всему миру. Она возвращает информацию о погоде в формате JSON. Например, чтобы получить текущую температуру в Лондоне, измеренную в градусах Фаренгейта (units=imperial), используйте следующий URL:

/

  find?q=London&units=imperial&appid=12345

Чтобы задействовать этот сервис, перейдите по ссылке openweathermap.org и получите идентификатор приложения (application ID, appid). Код, показанный в листинге 5.5, создает URL путем конкатенации базового URL, введенного имени города и идентификатора приложения. Когда пользователь вводит имя города, код подписывается на поток событий и отправляет запросы HTTP. Если новый запрос отправляется до того, как приложение получает результат предыдущего запроса, то оператор switchMap() отменяет предыдущий запрос и отправляет новый. Отмену ожидающих запросов нельзя выполнить, применяя ожидания. В этом примере также используется директива FormControl, с ее помощью генерируется наблюдаемый поток для поля ввода, где пользователь набирает название города.

Листинг 5.5. Содержимое файла observable-events-

73510.png 

76420.png 

Обратите внимание на два наблюдаемых потока в листинге 5.5:

директива FormControl создает наблюдаемый поток для событий поля ввода (this.searchInput.valueChanges);

• метод getWeather() тоже возвращает наблюдаемый поток.

Вы используете оператор switchMap() вместо subscribe, когда функция, обрабатывающая сгенерированные наблюдаемым потоком данные, также может возвращать наблюдаемый поток. Далее вы применяете метод subscribe() для второго наблюдаемого потока:

Observable1 ➔ switchMap(function) ➔ Observable2 ➔ subscribe()

Вы переключаетесь с первого наблюдаемого потока на второй. Если поток Observable1 отправит новое значение, но функция, которая создает Observable2, еще не закончила свою работу, завершается принудительно. Метод switchMap() отписывается, подписывается заново на Observable1 и начинает обрабатывать новое значение из этого потока.

Что, если наблюдаемый поток, поступающий из интерфейса, отправляет новое значение до того, как метод getWeather() вернет наблюдаемое значение? В этом случае метод switchMap() завершает запущенный вызов getWeather(), получает новое значение для названия города, пришедшего из интерфейса, и снова вызывает метод getWeather(). При завершении вызова getWeather() он также отменяет HTTP-запрос, который выполнялся медленно и не успел закончиться в срок.

Первый аргумент метода subscribe() содержит метод обратного вызова для обработки данных, поступающих с сервера. Код в выражении со стрелкой специ­фичен для API, предоставленного сервисом погоды. Вы должны извлечь значение температуры и влажности из возвращенной строки в формате JSON. API, предлагаемый именно этим сервисом, сохраняет коды ошибок в ответе, так что вы вручную обрабатываете статус 404, не прибегая к использованию функции обратного вызова.

Теперь убедимся в отмене предыдущего запроса. Для ввода слова London нужно больше чем 200 миллисекунд, указанных для метода debounceTime(). Это значит, что событие valueChanges отправит наблюдаемые данные больше одного раза. Для гарантии того, что запрос на сервер длится дольше 200 миллисекунд, вам нужно медленное соединение с Интернетом.

примечание

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

Мы запустили предыдущий пример, а затем включили регулирование на панели Developer Tools (Инструменты разработчика) браузера Chrome, чтобы эмулировать медленное соединение GPRS. В результате при вводе слова London было сгенерировано четыре вызова метода getWeather() для сочетаний Lo, Lon, Lond и London. Соответственно мы отправили четыре HTTP-запроса, и три из них были автоматически отменены оператором switchMap(), как показано на рис. 5.10.

73519.png 

Рис. 5.10. Запуск файла observable_events_http.ts

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

Фильтры — еще одна особенность Angular, позволяющая делать больше и при этом писать меньше кода.

примечание

В главе 9 содержится переработанная версия погодного приложения.

5.3. Каналы

Канал — это элемент шаблона, позволяющий преобразовывать значение в жела­емый вид. Указывается путем добавления вертикальной полоски (|) и имени канала сразу после значения, которое нужно преобразовать:

template: `<p>Your birthday is {{ birthday | date }}</p>`

Angular поставляется с несколькими заранее определенными каналами, каждый из них имеет класс, в котором реализована его функциональность (например, DatePipe), а также имя, пригодное к использованию в шаблоне (например, date):

UpperCasePipe позволяет преобразовывать входную строку в такую же строку в верхнем регистре, применяя конструкцию | uppercase, размещаемую в шаблоне;

• DatePipe дает возможность отображать дату в разных форматах с помощью конструкции | date;

• CurrencyPipe преобразует число в желаемую валюту, задействуя конструкцию | currency;

• AsyncPipe поможет извлечь данные из обертки, роль которой выполняет наблюдаемый поток, с помощью конструкции | async. Вы увидите пример кода, использующего эту особенность, в главе 8.

Для некоторых каналов входные параметры не нужны (например, uppercase), для других же — наоборот (например, date:'medium'). Можно объединить в цепочку столько каналов, сколько необходимо. В следующем фрагменте кода показывается, как отобразить значение переменной birthday в формате medium date и верхнем регистре (например, JUN 15, 2001, 9:43:11 PM):

template=

    `<p>

        {{ birthday | date:'medium' | uppercase}}

    </p>`

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

Обходное решение для неработающих каналов

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

Добавить сервис polyfill в ваш файл index.html:

<script src="/

76440.png polyfill.min.js?features=Intl.~locale.en"></script>

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

Если не хотите загружать сценарии из CDN (или это запрещено), то добавьте в ваш проект пакет internationalization:

npm install [email protected] --save

Затем добавьте следующие строки в ваш файл index.html:

<script src="node_modules/intl/dist/Intl.min.js"></script>

<script src="node_modules/intl/locale-data/jsonp/en.js"></script>

Второе решение увеличит размер вашего приложения на 33 Кбайт.

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

5.3.1. Пользовательские каналы

Помимо заранее определенных каналов, Angular предлагает простой способ создания пользовательских каналов, которые могут содержать код, характерный именно для вашего приложения. Нужно создать класс с аннотацией @Pipe, реализующий интерфейс PipeTransform. Он имеет следующую сигнатуру:

export interface PipeTransform {

  transform(value: any, ...args: any[]): any;

}

Это говорит о том, что пользовательский класс канала должен реализовать всего один метод, имеющий показанную сигнатуру. Первый параметр transform принимает преобразуемое значение, а второй определяет ноль или больше параметров, необходимых вашему алгоритму преобразования. Имя фильтра, используемое в шаблоне, следует указать в аннотации @Pipe. Если ваш компонент применяет пользовательские каналы, то их нужно явно указать в свойстве pipes аннотации @Component.

В предыдущем разделе пример с погодой показал температуру в Лондоне, измеренную в градусах Фаренгейта. Но в большинстве стран используется метрическая система и температура измеряется в градусах Цельсия. Напишем пользовательский канал, который преобразует температуру из градусов Фаренгейта в градусы Цельсия и наоборот. Код нашего канала (листинг 5.6) может быть применен в шаблоне под именем temperature.

Листинг 5.6. Содержимое файла temperature-pipe.ts

73539.png 

Далее показан код компонента (файл pipe-tester.ts), который использует канал temperature (листинг 5.7). Изначально эта программа будет преобразовывать температуру из градусов Фаренгейта в градусы Цельсия (формат FtoC). После нажатия кнопки вы можете изменить направление преобразования температуры.

Листинг 5.7. Содержимое файла pipe-tester.ts

73552.png 

76446.png 

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

5.4. Практикум: фильтрация продуктов онлайн-аукциона

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

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

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

примечание

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

Следуйте шагам, описанным ниже.

1. Создайте пользовательский канал, FilterPipe. Для этого создайте новый подкаталог pipes, в который нужно поместить следующий файл filter-pipe.ts, реализуя пользовательский канал FilterPipe (листинг 5.8).

Листинг 5.8. Содержимое файла filter-pipe.ts

73566.png 

2. Измените компонент HomeComponent так, чтобы он использовал FilterPipe. Компонент HomeComponent является предком для элементов <auction-product-item>, а также имеет переменную products, в которой хранится массив продуктов, предоставляемых сервисом ProductService. Канал FilterPipe будет отфильтровывать элементы этого массива.

Вам нужно добавить элемент <input>, куда пользователь может вводить критерий для фильтрации. Компонент HomeComponent подпишется на наблюдаемый поток событий, исходящий из этого поля ввода, чтобы получать значение для фильтра.

Наконец, вам нужно использовать пользовательский канал; это включает в себя следующие действия:

• импорт FilterPipe;

• включение FilterPipe в раздел declarations аннотации @NgModule;

• применение канала в цикле *ngFor для каждого элемента массива.

Измените код файла home.ts, чтобы он выглядел так, как показано в листин­ге 5.9.

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

73577.png 

76451.png 

3. Добавьте модуль ReactiveFormsModule в раздел imports директивы @NgModule файла app.module.ts. Он нужен для того, чтобы у вас был доступ к элементу filter <input>.

4. Запустите приложение, введя команду npm start в командной строке. Начните вводить какое-нибудь название продукта в поле filter и увидите, что браузер отрисовывает только те продукты, которые соответствуют критерию фильтра.

Для сравнения, мы запустили приложение-аукцион и приостановили его в отладчике после того, как ввели букву f в поле filter. На рис. 5.11 показано значение, полученное фильтром FilterPipe до того, как была выполнена фильтрация.

73586.png 

Рис. 5.11. Фильтр FilterPipe получает значение values

На панели Watch (Наблюдаемые значения) вы можете увидеть значения, полученные фильтром FilterPipe в качестве параметров. Стрелки показывают в действии деструктурирование TypeScript. После завершения фильтрации в этом окне показываются только те продукты, которые содержат в своих названиях букву f. В данном упражнении вы реализовали наблюдаемый поток событий и пользовательский фильтр.

5.5. Резюме

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

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

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

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

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

• Двухсторонняя привязка обозначается нотацией [()].

• Структурную директиву ngIf можно использовать для добавления и удаления узлов из модели DOM браузера.

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

Назад: 4. Внедрение зависимостей
Дальше: 6. Реализация коммуникации между компонентами

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