Angular 2 Beta, обучающий курс «Тур героев» часть 3

в 16:55, , рубрики: angular 2.0, angular2, AngularJS, javascript, tour of heroes, tutorial, Разработка веб-сайтов

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

Запустить приложение, часть 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. Это не лучший вариант. Мы должны сделать рефакторинг доступа к данным, вынеся получение данных в отдельный сервис, и расшарить этот сервис компонентам, которым необходимы эти данные.

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

Автор: illian

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js