Редактирование

Итак, мы вывели список друзей. Создадим возможность редактирования отдельного контакта. Сохранять изменённые данные на сервере не будем, т.к. для этого нужен бэкенд и API. А это тема для отдельного курса.

Сперва создаём новый компонент, который будет отвечать за вывод информации на редактирование. Файл с расширением .spec.ts удаляем.

ng generate component friend-detail

Теперь сделаем так, чтобы компонент принимал в себя экземпляр объекта типа Friend. Для этого внесём такие изменения в класс компонента ./src/app/friend-detail/friend-detail.component.ts:

import { Component, Input } from '@angular/core'; // Делаем доступным для использования декоратор @Input()
import { Friend } from '../friend';

@Component({
  selector: 'app-friend-detail',
  templateUrl: './friend-detail.component.html',
  styleUrls: ['./friend-detail.component.css']
})
export class FriendDetailComponent {

  @Input() friend: Friend; // Декоратор @Input() сообщает, что значение переменной задаёт родительский компонент

  constructor() { }

}

Зададим шаблон компонента. Для этого открываем на редактирование шаблон ./src/app/friend-detail/friend-detail.component.html:

<div *ngIf="friend" class="friend-card">
	<!-- Структурная директива *ngIf делает зависимым отображение содержимого от переменной this.friend. Если она пуста, тогда то, что внутри, не будет отображаться. -->
</div>

Структурная директива *ngIf проверяет существование переменной friend, той самой, для которой мы задавали @Input в коде класса листингом выше. Это нужно, т.к. в дальнейшем мы будем отправлять друга на редактирование кликом по его имени в списке. А значит, при загрузке по умолчанию переменная friend не будет задана.

Добавим в этот же файл поля ввода:

<div *ngIf="friend" class="friend-card">

  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_double friend-card__cell_ta-center">
      <h2>{{ friend.name }}</h2>
    </div>
  </div>
  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
      <span>ID:</span>
    </div>
    <div class="friend-card__cell friend-card__cell_width-60">
      <span>{{ friend._id }}</span>
    </div>
  </div>
  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
      <label for="friend-card__name">Имя:</label>
    </div>
    <div class="friend-card__cell friend-card__cell_width-60">
      <input id="friend-card__name" type="text" [(ngModel)]="friend.name" placeholder="Имя друга"/>
    </div>
  </div>

</div>

На данный момент приложение пока выдаёт ошибку, но это мы исправим чуть позже.

Самая и интересная строчка в листинге выше, это <input id="friend-card__name" type="text" [(ngModel)]="friend.name" placeholder="Имя друга"/>. Конструкция [(ngModel)] делает двустороннее связывание. Или «two-way data binding» в англоязычной документации. Она привязывает текущее input-поле к свойству name объекта friend.

Главному модулю надо рассказать о компоненте и об использовании возможностей модуля форм (FormsModule). Для этого редактируем ./src/app/app.module.ts.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms'; // Модуль для работы с формами

import { AppComponent } from './app.component';
import { FriendDetailComponent } from './friend-detail/friend-detail.component'; // Добавляем новый, только что созданный компонент.

@NgModule({
  declarations: [
    AppComponent,
    FriendDetailComponent // Теперь подключаем его.
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    FormsModule // И подключаем модуль форм.
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

А сейчас нам нужно реализовать функцию выбора друга и сохранение его в свойстве основного компонента (в AppComponent). Делается это через шаблон основного компонента ./src/app/app.component.html. Внимательно читаем комментарии в листингах.

<div class="outer">
  <div class="toppanel">

  </div>
  <div class="header">
    <div class="header__logo"><img class="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="></div>
    <div class="header__header"><h1>Список контактов</h1></div>
  </div>
  <app-friend-detail [friend]="selectedFriend"></app-friend-detail>
  <ul class="friends-list">
    <li *ngFor="let friend of friends"> <!-- friend содержит разных друзей на каждой итерации перечисления элементов массива this.friends -->
      <span
        (click)="selectFriend(friend)" !-- Локальную переменную friend по событию клика отправляем в качестве аргумента в функцию selectFriend() -->
        class="friend-name"
      >{{friend.name}}</span>
    </li>
  </ul>
</div>

Событие клика заключено в круглые скобки (click), это называется привязкой события («event binding»).

Метод selectFriend(friend) из основного компонента сохраняет полученную переменную в this.selectedFriend.

Основной компонент (./src/app/app.component.ts):

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Friend } from './friend';
import { FriendsService } from './friends.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {

  friends: Friend[];
  selectedFriend: Friend;
  subscription: Subscription;

  constructor (private friendsService: FriendsService) {}

  getFriends(): void {
    this.subscription = this.friendsService.getFriends()
      .subscribe(result => {
        this.friends = result;
      });
  }

  selectFriend(friend: Friend): void {
    this.selectedFriend = friend;
  }

  ngOnInit() {
    this.getFriends();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

Добавим компонент FriendDetailComponent в шаблон AppComponent. ./src/app/app.component.html:

<div class="outer">
  <div class="toppanel">

  </div>
  <div class="header">
    <div class="header__logo"><img class="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="></div>
    <div class="header__header"><h1>Список контактов</h1></div>
  </div>
  <app-friend-detail [friend]="selectedFriend"></app-friend-detail>
  <ul class="friends-list">
    <li *ngFor="let friend of friends">
      <span (click)="selectFriend(friend)" class="friend-name">{{friend.name}}</span>
    </li>
  </ul>
</div>

Откуда взялся <app-friend-detail>? Селектор компонента, которым он будет вызываться в шаблонах других компонентах, описывает в декораторе @Component в свойстве selector, вы это можете увидеть в файле ./src/app/friend-detail/friend-detail.component.ts.

В элементе <app-friend-detail> конструкция [friend]="selectedFriend" пробрасывает переменную selectedFriend компонента AppComponent в переменную @Input friend: Friend; компонента FriendDetailComponent. Это называется односторонней привязкой. Если потребуется гуглить в англоязычном сегменте интернета, там оно называется «one-way data binding».

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

Добавим стили компонента в ./src/app/friend-detail/friend-detail.component.css:

.friend-card {
  margin:2px;
  position:relative;
  overflow:hidden;
  padding:0 0 12px 0;
}
.friend-card:before {
  display:block;
  content:"";
  position:absolute;
  top:0;
  right:0;
  bottom:0;
  left:0;
  background:#fff;
  opacity:.4;
  -webkit-box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.75);
  -moz-box-shadow:    0 3px 15px 0 rgba(0, 0, 0, 0.75);
  box-shadow:         0 3px 15px 0 rgba(0, 0, 0, 0.75);
  z-index:1;
}
.friend-card__row {
  position:relative;
  width:100%;
  display:flex;
  justify-content:space-between;
  z-index:2;
}
.friend-card__cell {
  padding:4px;
  line-height:36px;
}
.friend-card__cell h1,
.friend-card__cell .h1,
.friend-card__cell h2,
.friend-card__cell .h2,
.friend-card__cell h3,
.friend-card__cell .h3 {
  margin:6px auto;
}
.friend-card__cell_ta-right {
  text-align:right;
}
.friend-card__cell_ta-center {
  text-align:center;
}
.friend-card__cell_ta-center h1,
.friend-card__cell_ta-center .h1,
.friend-card__cell_ta-center h2,
.friend-card__cell_ta-center .h2,
.friend-card__cell_ta-center h3,
.friend-card__cell_ta-center .h3 {
  text-align:center;
}
.friend-card__cell_double {
  width:100%;
}
.friend-card__cell_width-40 {
  width:40%;
}
.friend-card__cell_width-60 {
  width:60%;
}

То, что получилось, я собрал командой в билд:

ng build --prod --base-href /builds/angular-6-2/lesson01/01/

Команда ng build собирает проект. Флаг --prod делает tree shaking — «трясёт дерево» кода на предмет неиспользуемых функций. И подготавливает проект к продакшену. Параметр --base-href нужен, когда билд будет размещён не в корне.

P.S. Для создания билда, который будет размещён в корне, достаточно команды ng build --prod.

Результат

Результат 1-го урока.
Кликайте по другу и пробуйте редактировать имя.
Результирующие листинги изменённых файлов
import { Component, Input } from '@angular/core';
import { Friend } from '../friend';

@Component({
  selector: 'app-friend-detail',
  templateUrl: './friend-detail.component.html',
  styleUrls: ['./friend-detail.component.css']
})
export class FriendDetailComponent {

  @Input() friend: Friend;

  constructor() { }

}

<div *ngIf="friend" class="friend-card">

  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_double friend-card__cell_ta-center">
      <h2>{{ friend.name }}</h2>
    </div>
  </div>
  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
      <span>ID:</span>
    </div>
    <div class="friend-card__cell friend-card__cell_width-60">
      <span>{{ friend._id }}</span>
    </div>
  </div>
  <div class="friend-card__row">
    <div class="friend-card__cell friend-card__cell_width-40 friend-card__cell_ta-right">
      <label for="friend-card__name">Имя:</label>
    </div>
    <div class="friend-card__cell friend-card__cell_width-60">
      <input id="friend-card__name" type="text" [(ngModel)]="friend.name" placeholder="Имя друга"/>
    </div>
  </div>

</div>

.friend-card {
  margin:2px;
  position:relative;
  overflow:hidden;
  padding:0 0 12px 0;
}
.friend-card:before {
  display:block;
  content:"";
  position:absolute;
  top:0;
  right:0;
  bottom:0;
  left:0;
  background:#fff;
  opacity:.4;
  -webkit-box-shadow: 0 3px 15px 0 rgba(0, 0, 0, 0.75);
  -moz-box-shadow:    0 3px 15px 0 rgba(0, 0, 0, 0.75);
  box-shadow:         0 3px 15px 0 rgba(0, 0, 0, 0.75);
  z-index:1;
}
.friend-card__row {
  position:relative;
  width:100%;
  display:flex;
  justify-content:space-between;
  z-index:2;
}
.friend-card__cell {
  padding:4px;
  line-height:36px;
}
.friend-card__cell h1,
.friend-card__cell .h1,
.friend-card__cell h2,
.friend-card__cell .h2,
.friend-card__cell h3,
.friend-card__cell .h3 {
  margin:6px auto;
}
.friend-card__cell_ta-right {
  text-align:right;
}
.friend-card__cell_ta-center {
  text-align:center;
}
.friend-card__cell_ta-center h1,
.friend-card__cell_ta-center .h1,
.friend-card__cell_ta-center h2,
.friend-card__cell_ta-center .h2,
.friend-card__cell_ta-center h3,
.friend-card__cell_ta-center .h3 {
  text-align:center;
}
.friend-card__cell_double {
  width:100%;
}
.friend-card__cell_width-40 {
  width:40%;
}
.friend-card__cell_width-60 {
  width:60%;
}

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { FriendDetailComponent } from './friend-detail/friend-detail.component';

@NgModule({
  declarations: [
    AppComponent,
    FriendDetailComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    FormsModule
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Friend } from './friend';
import { FriendsService } from './friends.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {

  friends: Friend[];
  selectedFriend: Friend;
  subscription: Subscription;

  constructor (private friendsService: FriendsService) {}

  getFriends(): void {
    this.subscription = this.friendsService.getFriends()
      .subscribe(result => {
        this.friends = result;
      });
  }

  selectFriend(friend: Friend): void {
    this.selectedFriend = friend;
  }

  ngOnInit() {
    this.getFriends();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

<div class="outer">
  <div class="toppanel">

  </div>
  <div class="header">
    <div class="header__logo"><img class="logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="></div>
    <div class="header__header"><h1>Список контактов</h1></div>
  </div>
  <app-friend-detail [friend]="selectedFriend"></app-friend-detail>
  <ul class="friends-list">
    <li *ngFor="let friend of friends">
      <span (click)="selectFriend(friend)" class="friend-name">{{friend.name}}</span>
    </li>
  </ul>
</div>