Часть 1 Часть 2 Часть 3


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


Запустить приложение, часть 3


Где мы остановились


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


    angular2-tour-of-heroes
        app
            app.component.ts
            main.ts
        node_modules ...
        typings ...
        index.html
        package.json
        tsconfig.json
        typings.json

Поддержка преобразования кода и выполнения приложения


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


    npm start

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


Создание компонента детальной информации о герое


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


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


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


Отделяем детальную информацию о герое


Добавим новый файл с именем hero-detail.component.ts в папку app и создадим HeroDetailComponent, как показано ниже.


hero-detail.component.ts (первоначальная версия)


    import {Component, Input} from 'angular2/core';

    @Component({
      selector: 'my-hero-detail',
    })
    export class HeroDetailComponent {
    }

Cоглашения об именовании

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


Обратите внимание на то, что у нас есть AppComponent в файле с именем app.component.ts и наш новый HeroDetailComponent находится в файле с именем hero-detail.component.ts.


Все наши составные имена классов заканчиваются на "Component". Все наши составные имена файлов заканчиваются на ".component".


Мы переводим имена файлов в "нижний регистр с тире" (kebab-case), поэтому мы не беспокоимся о чувствительности к регистру на сервере или в системе управления версиями.


Рассмотрим вышеприведенный код.


Мы начали с импорта декораторов Angular — Component и Input, потому что они скоро нам понадобятся.
Затем мы создаем метаданные с декоратором @Component, где мы указываем имя селектора, который идентифицирует элемент компонента. Затем мы экспортируем класс, чтобы сделать его доступным для других компонентов.


Закончив здесь, мы импортируем его в AppComponent и создадим соответствующий элемент
<my-hero-detail>.


Шаблон детальной информации о герое


В данный момент, представления Heroes и Hero Detail объединены в один шаблон в AppComponent. Давайте вырежем содержимое Hero Detail из AppComponent и вставим его в новое свойство шаблона HeroDetailComponent.


Ранее мы привязывали свойство selectedHero.name в AppComponent. Наш HeroDetailComponent будет иметь свойство hero, а не свойство selectedHero. Таким образом, мы заменим selectedHero на hero повсюду в нашем новом шаблоне. Это наше единственное изменение. Результат будет выглядеть так:


hero-detail.component.ts (шаблон)


    template: `
      <div *ngIf="hero">
        <h2>{{hero.name}} details!</h2>
        <div><label>id: </label>{{hero.id}}</div>
        <div>
          <label>name: </label>
          <input [(ngModel)]="hero.name" placeholder="name"/>
        </div>
      </div>
    `

Теперь наша разметка детальной информации о герое существует только в HeroDetailComponent.


Добавление свойства hero


Добавим свойство hero, о котором мы говорили выше, к классу компонента.


    hero: Hero;

Ой-ой. Мы объявили свойство hero как тип Hero, но наш класс героя находится в файле app.component.ts. У нас есть два компонента, каждый из которых в своем собственном файле, которые должны ссылаться на класс Hero.


Мы решим эту проблему, переместив класс Hero из app.component.ts в свой собственный файл hero.ts.


hero.ts (Экспортированный класс Hero)


    export class Hero {
      id: number;
      name: string;
    }

Мы экспортируем класс Hero из hero.ts, потому что нам нужно ссылаться на него в обоих файлах компонентов. Добавьте следующий оператор импорта в верхней части app.component.ts и hero-detail.component.ts.


hero-detail.component.ts и app.component.ts (Импорт класса Hero)


    import {Hero} from './hero';

Свойство hero является входящим.


Нужно сказать компоненту HeroDetailComponent, какого героя ему отобразить. Кто ему скажет это? Родитель AppComponent!


AppComponent знает, какого героя показать: героя, который пользователь выбрал из списка. Выбор пользователя находится в свойстве selectedHero.


Мы обновим шаблон AppComponent так, что он свяжет свое свойство selectedHero со свойством hero нашего HeroDetailComponent. Связывание может выглядеть следующим образом:


    <my-hero-detail [hero]="selectedHero"></my-hero-detail>

Обратите внимание на то, что свойство hero является целевым свойством — оно в квадратных скобках слева от (=).


Angular требует, чтобы объявленное целевое свойство было входящим свойством. Если мы это не сделаем, Angular откажет в связывании и выдаст сообщение об ошибке.


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

Есть несколько способов, как указать, что hero является входящим. Мы сделаем это предпочтительным способом, аннотировав свойство hero декоратором @Input, которое мы импортировали ранее.


    @Input() 
    hero: Hero;

Узнать больше о декораторе @Input() можно в главе Директивы атрибутов.

Обновление AppComponent


Вернемся к AppComponent и научим его использовать HeroDetailComponent.


Начнем с импорта HeroDetailComponent, чтобы можно было сослаться на него.


    import {HeroDetailComponent} from './hero-detail.component';

Найдем место в шаблоне, где мы удалили содержимое Hero Detail и добавим тег элемента, который представляет HeroDetailComponent.


    <my-hero-detail></my-hero-detail>

my-hero-detail — имя, которое мы установили в свойстве selector метаданных HeroDetailComponent.

Эти два компонента не будет скоординированы, пока мы не свяжем свойство selectedHero компонента AppComponent со свойством hero компонента HeroDetailComponent, например так:


    <my-hero-detail [hero]="selectedHero"></my-hero-detail>

Шаблон AppComponent должен выглядеть следующим образом:
app.component.ts (Шаблон)


    template:`
          <h1>{{title}}</h1>
          <h2>My Heroes</h2>
          <ul class="heroes">
            <li *ngFor="#hero of heroes"
              [class.selected]="hero === selectedHero"
              (click)="onSelect(hero)">
              <span class="badge">{{hero.id}}</span> {{hero.name}}
            </li>
          </ul>
          <my-hero-detail [hero]="selectedHero"></my-hero-detail>
        `,

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


Это пока что не происходит!


Щелкаем в списке героев. Никакой информации. Мы ищем ошибки в консоли "Инструменты разработчика браузера". Ошибок нет.


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


Массив директив


Браузер игнорирует неизвестные ему HTML теги и атрибуты. Так же поступает и Angular.


Мы импортировали HeroDetailComponent и использовали его в шаблоне, но мы не сказали Angular об этом.


Мы говорим об этом Angular путем перечисления этого компонента в метаданных, в массиве directives. Добавим этот массив свойств в нижней части конфигурации @Component, сразу после template и styles.


    directives: [HeroDetailComponent]

Заработало!


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


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


Мы создали наш первый повторно используемый компонент!


Обзор структуры приложения


Давайте проверим, что после проведенного в этой главе рефакторинга, у нас следующая структура проекта:


    angular2-tour-of-heroes
        app
            app.component.ts
            hero.ts
            hero-detail.component.ts
            main.ts
        node_modules ...
        typings ...
        index.html
        package.json
        tsconfig.json
        typings.json

Файлы кода, которые мы обсуждали в этой главе.


app/hero-detail.component.ts
    import {Component, Input} from 'angular2/core';
    import {Hero} from './hero';

    @Component({
      selector: 'my-hero-detail',
      template: `
        <div *ngIf="hero">
          <h2>{{hero.name}} details!</h2>
          <div><label>id: </label>{{hero.id}}</div>
          <div>
            <label>name: </label>
            <input [(ngModel)]="hero.name" placeholder="name"/>
          </div>
        </div>
      `
    })
    export class HeroDetailComponent {
      @Input() 
      hero: Hero;
    }

app/app.component.ts
    import {Component} from 'angular2/core';
    import {Hero} from './hero';
    import {HeroDetailComponent} from './hero-detail.component';

    @Component({
      selector: 'my-app',
      template:`
        <h1>{{title}}</h1>
        <h2>My Heroes</h2>
        <ul class="heroes">
          <li *ngFor="#hero of heroes"
            [class.selected]="hero === selectedHero"
            (click)="onSelect(hero)">
            <span class="badge">{{hero.id}}</span> {{hero.name}}
          </li>
        </ul>
        <my-hero-detail [hero]="selectedHero"></my-hero-detail>
      `,
      styles:[`
        .selected {
          background-color: #CFD8DC !important;
          color: white;
        }
        .heroes {
          margin: 0 0 2em 0;
          list-style-type: none;
          padding: 0;
          width: 15em;
        }
        .heroes li {
          cursor: pointer;
          position: relative;
          left: 0;
          background-color: #EEE;
          margin: .5em;
          padding: .3em 0;
          height: 1.6em;
          border-radius: 4px;
        }
        .heroes li.selected:hover {
          background-color: #BBD8DC !important;
          color: white;
        }
        .heroes li:hover {
          color: #607D8B;
          background-color: #DDD;
          left: .1em;
        }
        .heroes .text {
          position: relative;
          top: -3px;
        }
        .heroes .badge {
          display: inline-block;
          font-size: small;
          color: white;
          padding: 0.8em 0.7em 0 0.7em;
          background-color: #607D8B;
          line-height: 1em;
          position: relative;
          left: -1px;
          top: -4px;
          height: 1.8em;
          margin-right: .8em;
          border-radius: 4px 0 0 4px;
        }
      `],
      directives: [HeroDetailComponent]
    })
    export class AppComponent {
      title = 'Tour of Heroes';
      heroes = HEROES;
      selectedHero: Hero;
      onSelect(hero: Hero) { this.selectedHero = hero; }
    }

    var HEROES: Hero[] = [
      { "id": 11, "name": "Mr. Nice" },
      { "id": 12, "name": "Narco" },
      { "id": 13, "name": "Bombasto" },
      { "id": 14, "name": "Celeritas" },
      { "id": 15, "name": "Magneta" },
      { "id": 16, "name": "RubberMan" },
      { "id": 17, "name": "Dynama" },
      { "id": 18, "name": "Dr IQ" },
      { "id": 19, "name": "Magma" },
      { "id": 20, "name": "Tornado" }
    ];

app/hero.ts
    export class Hero {
      id: number;
      name: string;
    }

Путь, который мы прошли


Давайте подведем итоги того, что мы создали.


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

Запустить приложение, часть 3


Предстоящий путь


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


Мы все еще получаем наши данные о героях (используя заглушку для их получения) в AppComponent. Это не лучший вариант. Мы должны сделать рефакторинг доступа к данным, вынеся получение данных в отдельный сервис, и расшарить этот сервис компонентам, которым необходимы эти данные.


Мы будем учиться создавать сервисы в следующей главе.

Комментарии (12)


  1. Andris_habrahabr
    28.04.2016 14:15

    Очень доступно изложено, спасибо!
    Ждём продолжения :)


  1. Focushift
    28.04.2016 14:16
    -1

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


    1. Sterhel
      28.04.2016 14:17
      +1

      Делаем 1 осознанный клик по никнейму автора над заголовком – и получаем все 3 публикации серии, https://habrahabr.ru/users/illian/topics/


  1. Roms
    28.04.2016 15:00
    +1

    Думаю стоит добавить ссылки и на предыдущие части:
    Angular 2 Beta, обучающий курс «Тур героев» часть 1
    Angular 2 Beta, обучающий курс «Тур героев» часть 2



    1. illian
      28.04.2016 19:37
      +2

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


  1. EJIqpEP
    29.04.2016 10:05

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


    1. EJIqpEP
      29.04.2016 10:29

      Почему то ссылка на прикрепилась. http://monsterlessons.com/project/lessons/tur-geroev-vstuplenie


  1. bromzh
    29.04.2016 14:41

    В beta.17 вышли breaking change для синтаксиса ngFor.
    Суть такова: теперь #foo всегда будет локальной ссылкой на элемент. var-биндинг исчез, вместо него теперь let-биндинг.
    Теперь, вместо <div *ngFor="#item of items"> надо писать <div *ngFor="let item of items">.


    Такой код:


    <ul>
      <li *ngFor="let item in items">{{item}}</li>
    </ul>

    Раскрывается в такой:


    <ul>
      <template ngFor let-item [ngForOf]='items'><li>{{item}}</li></template>
    </ul>

    https://github.com/angular/angular/issues/7158


  1. senyaarseniy
    30.04.2016 20:30

    Хороший перевод спс)
    Не подскажите где можно былобы достать список ивентов которые можно использовать в html файла? Обыскал весь инет ничего не могу найти(


    1. illian
      30.04.2016 20:33

      Не совсем понял вопрос. Список директив в html-шаблоне?


      1. senyaarseniy
        30.04.2016 21:30

        Уже нашёл ответ, cпасибо, беруться стандартные ивенты без префикса «on» (onClick => click)