В первой части этой серии мы узнали, как запустить наше приложение Todo и развернуть его на страницах GitHub. Это работало просто отлично, но, к сожалению, все приложение было собрано в один компонент. В этой статье мы рассмотрим более модульную компонентную архитектуру. Мы рассмотрим, как разбить этот отдельный компонент на структурированное дерево более мелких компонентов, которые легче понять, использовать повторно и поддерживать.
Эта статья является второй частью Учебника SitePoint Angular 2+ о том, как создать приложение CRUD с помощью Angular CLI .
- Часть 0 — Ultimate Angular CLI Справочное руководство
- Часть 1. Подготовка и запуск нашей первой версии приложения Todo
- Часть 2. Создание отдельных компонентов для отображения списка задач и одной задачи
- Часть 3. Обновление сервиса Todo для связи с REST API
- Часть 4. Использование углового маршрутизатора для разрешения данных .
- Часть 5. Добавление аутентификации для защиты частного контента.
- Часть 6 — Как обновить Angular Projects до последней версии.
Вам не нужно следовать первой части этого урока, чтобы вторая часть имела смысл. Вы можете просто взять копию нашего репо, получить код из первой части и использовать его в качестве отправной точки. Это объясняется более подробно ниже.
Краткий обзор
Итак, давайте посмотрим на то, что мы рассмотрели в первой части, чуть подробнее. Мы научились:
- инициализировать наше приложение Todo с помощью Angular CLI
- создать класс
Todo
для представления отдельных задач - создать сервис
TodoDataService
для создания, обновления и удаления задач - используйте компонент
AppComponent
для отображения пользовательского интерфейса - разверните наше приложение на страницах GitHub.
Архитектура приложения части 1 выглядела так:
Компоненты, которые мы обсуждали, отмечены красной рамкой.
Во второй статье мы AppComponent
часть работы, AppComponent
, более мелким компонентам, которые легче понять, использовать повторно и поддерживать.
Мы создадим:
-
TodoListComponent
для отображения списка задач -
TodoListItemComponent
для отображения одного todo -
TodoListHeaderComponent
для создания новой задачи -
TodoListFooterComponent
чтобы показать, сколькоTodoListFooterComponent
.
К концу этой статьи вы поймете:
- основы угловой компонентной архитектуры
- как вы можете передавать данные в компонент, используя привязки свойств
- как вы можете прослушивать события, испускаемые компонентом, используя прослушиватели событий
- почему рекомендуется разбивать компоненты на компоненты многократного использования
- Разница между умными и немыми компонентами и то, почему держать компоненты немыми — это хорошая практика.
Итак, начнем!
Вверх и работает
Первое, что вы должны будете следовать этой статье, это последняя версия Angular CLI. Вы можете установить это с помощью следующей команды:
npm install -g @angular/cli@latest
Если вам нужно удалить предыдущую версию Angular CLI, вот как:
npm uninstall -g @angular/cli angular-cli npm cache clean npm install -g @angular/cli@latest
После этого вам понадобится копия кода из первой части. Это доступно по адресу https://github.com/sitepoint-editors/angular-todo-app . Каждая статья в этой серии имеет соответствующий тег в репозитории, чтобы вы могли переключаться между различными состояниями приложения.
Код, который мы закончили в первой части и с которого мы начинаем в этой статье, помечен как часть-1 . Код, которым мы заканчиваем эту статью, помечен как часть-2 .
Вы можете думать о тегах как псевдоним определенного идентификатора коммита. Вы можете переключаться между ними, используя git checkout
. Вы можете прочитать больше об этом здесь .
Итак, чтобы запустить и запустить (установлена последняя версия Angular CLI), мы должны сделать:
git clone [email protected]:sitepoint-editors/angular-todo-app.git cd angular-todo-app npm install git checkout part-1 ng serve
Затем посетите http: // localhost: 4200 / . Если все хорошо, вы должны увидеть работающее приложение Todo.
Оригинальный AppComponent
Давайте откроем src/app/app.component.html
и посмотрим на AppComponent
которым мы закончили в первой части:
<section class="todoapp"> <header class="header"> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()"> </header> <section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete"> <label>{{todo.title}}</label> <button class="destroy" (click)="removeTodo(todo)"></button> </div> </li> </ul> </section> <footer class="footer" *ngIf="todos.length > 0"> <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span> </footer> </section>
Вот соответствующий ему класс в src/app/app.component.ts
:
import {Component} from '@angular/core'; import {Todo} from './todo'; import {TodoDataService} from './todo-data.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [TodoDataService] }) export class AppComponent { newTodo: Todo = new Todo(); constructor(private todoDataService: TodoDataService) { } addTodo() { this.todoDataService.addTodo(this.newTodo); this.newTodo = new Todo(); } toggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } }
Хотя наш AppComponent
технически работает нормально, хранить весь код в одном большом компоненте плохо масштабируется и не рекомендуется.
Добавление дополнительных функций в наше приложение Todo сделает AppComponent
больше и сложнее, что AppComponent
его понимание и поддержку.
Поэтому рекомендуется делегировать функциональность меньшим компонентам. В идеале меньшие компоненты должны быть конфигурируемыми, чтобы нам не приходилось переписывать их код при изменении бизнес-логики.
Например, в третьей части этой серии мы обновим TodoDataService
для связи с REST API, и мы хотим убедиться, что нам не придется изменять какие-либо меньшие компоненты при рефакторинге TodoDataService
.
Если мы посмотрим на шаблон AppComponent
, мы можем извлечь его базовую структуру как:
<!-- header that lets us create new todo --> <header></header> <!-- list that displays todos --> <ul class="todo-list"> <!-- list item that displays single todo --> <li>Todo 1</li> <!-- list item that displays single todo --> <li>Todo 2</li> </ul> <!-- footer that displays statistics --> <footer></footer>
Если мы переведем эту структуру в имена компонентов Angular, мы получим:
<!-- TodoListHeaderComponent that lets us create new todo --> <app-todo-list-header></app-todo-list-header> <!-- TodoListComponent that displays todos --> <app-todo-list> <!-- TodoListItemComponent that displays single todo --> <app-todo-list-item></app-todo-list-item> <!-- TodoListItemComponent that displays single todo --> <app-todo-list-item></app-todo-list-item> </app-todo-list> <!-- TodoListFooterComponent that displays statistics --> <app-todo-list-footer></app-todo-list-footer>
Давайте посмотрим, как мы можем использовать возможности компонентно-ориентированной разработки Angular, чтобы это произошло.
Более модульная архитектура компонентов — создание компонента TodoListHeaderComponent
Начнем с создания компонента TodoListHeader
.
Из корня нашего проекта мы используем Angular CLI для генерации компонента для нас:
$ ng generate component todo-list-header
Это создает следующие файлы для нас:
create src/app/todo-list-header/todo-list-header.component.css create src/app/todo-list-header/todo-list-header.component.html create src/app/todo-list-header/todo-list-header.component.spec.ts create src/app/todo-list-header/todo-list-header.component.ts
Он автоматически добавляет TodoListHeaderComponent
в объявления AppModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; // Automatically imported by Angular CLI import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component'; @NgModule({ declarations: [ AppComponent, // Automatically added by Angular CLI TodoListHeaderComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Добавление компонента к объявлениям модуля необходимо, чтобы убедиться, что все шаблоны представлений в модуле могут использовать этот компонент. Angular CLI удобно добавил для нас TodoListHeaderComponent
, поэтому нам не нужно добавлять его вручную.
Если TodoListHeaderComponent
не было в объявлениях, и мы использовали его в шаблоне представления, Angular выдаст следующую ошибку:
Error: Uncaught (in promise): Error: Template parse errors: 'app-todo-list-header' is not a known element: 1. If 'app-todo-list-header' is an Angular component, then verify that it is part of this module. 2. If 'app-todo-list-header' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.
Чтобы узнать больше о объявлениях модулей, обязательно ознакомьтесь с FAQ по угловым модулям .
Теперь, когда у нас есть все файлы, созданные для нашего нового TodoListHeaderComponent
, мы можем переместить элемент <header>
из src/app/app.component.html
в src/app/todo-list-header/todo-list-header.component.html
:
<header class="header"> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()"> </header>
Также добавьте соответствующую логику в src/app/todo-list-header/todo-list-header.component.ts
:
import { Component, Output, EventEmitter } from '@angular/core'; import { Todo } from '../todo'; @Component({ selector: 'app-todo-list-header', templateUrl: './todo-list-header.component.html', styleUrls: ['./todo-list-header.component.css'] }) export class TodoListHeaderComponent { newTodo: Todo = new Todo(); @Output() add: EventEmitter<Todo> = new EventEmitter(); constructor() { } addTodo() { this.add.emit(this.newTodo); this.newTodo = new Todo(); } }
Вместо внедрения TodoDataService
в наш новый TodoListHeaderComponent
для сохранения нового todo, мы генерируем событие add
и передаем новое todo в качестве аргумента.
Мы уже узнали, что синтаксис шаблона Angular позволяет нам прикрепить обработчик к событию. Например, рассмотрим следующий код:
<input (keyup.enter)="addTodo()">
Это говорит Angular запускать метод addTodo()
при нажатии клавиши ввода внутри ввода. Это работает, потому что событие keyup.enter
является событием, определенным структурой Angular.
Однако мы также можем позволить компоненту генерировать свои собственные пользовательские события, создав EventEmitter и декорировав его с помощью декоратора @Output () :
import { Component, Output, EventEmitter } from '@angular/core'; import { Todo } from '../todo'; @Component({ // ... }) export class TodoListHeaderComponent { // ... @Output() add: EventEmitter<Todo> = new EventEmitter(); addTodo() { this.add.emit(this.newTodo); this.newTodo = new Todo(); } }
Теперь мы можем назначить обработчик событий в шаблоне представления, используя синтаксис привязки событий Angular:
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
Каждый раз, когда мы вызываем add.emit(value)
в TodoListHeaderComponent
, будет вызываться обработчик onAddTodo($event)
, а $event
будет равен value
.
Это отделяет наш TodoListHeaderComponent
от TodoDataService
и позволяет родительскому компоненту решать, что должно произойти при создании нового задания.
Когда мы обновим TodoDataService
для связи с REST API в третьей части, нам не придется беспокоиться о TodoListHeaderComponent
поскольку он даже не знает о существовании TodoDataService
.
Смарт против тупых компонентов
Возможно, вы уже слышали о умных и тупых компонентах . TodoListHeaderComponent
от TodoDataService
делает компонент TodoListHeaderComponent
немым компонентом. Тупой компонент не знает, что происходит вне его. Он получает входные данные только через привязки свойств и только выдает выходные данные в виде событий.
Использование умных и немых компонентов — хорошая практика. Это значительно улучшает разделение задач, облегчая понимание и поддержку вашего приложения. Если ваша база данных или внутренний API-интерфейс изменится, вам не придется беспокоиться о своих тупых компонентах. Это также делает ваши немые компоненты более гибкими, позволяя вам легче использовать их в различных ситуациях. Если вашему приложению требуется один и тот же компонент дважды, когда один раз ему нужно записать данные в базу данных на стороне сервера, а в другой раз — в базу данных в памяти, тупой компонент позволяет выполнить именно это.
Итак, теперь, когда мы создали наш TodoListHeaderComponent
, давайте обновим наш шаблон AppComponent
чтобы использовать его:
<section class="todoapp"> <!-- header is now replaced with app-todo-list-header --> <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header> <section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete"> <label>{{todo.title}}</label> <button class="destroy" (click)="removeTodo(todo)"></button> </div> </li> </ul> </section> <footer class="footer" *ngIf="todos.length > 0"> <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span> </footer> </section>
Обратите внимание, как мы используем onAddTodo($event)
для захвата событий add
которые отправляются TodoListHeaderComponent
когда пользователь вводит новый заголовок TodoListHeaderComponent
:
<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
Мы добавляем обработчик onAddTodo()
в класс AppComponent
и удаляем логику, которая нам больше не нужна:
import {Component} from '@angular/core'; import {Todo} from './todo'; import {TodoDataService} from './todo-data.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [TodoDataService] }) export class AppComponent { // No longer needed, now handled by TodoListHeaderComponent // newTodo: Todo = new Todo(); constructor(private todoDataService: TodoDataService) { } // No longer needed, now handled by TodoListHeaderComponent // addTodo() { // this.todoDataService.addTodo(this.newTodo); // this.newTodo = new Todo(); // } // Add new method to handle event emitted by TodoListHeaderComponent onAddTodo(todo: Todo) { this.todoDataService.addTodo(todo); } toggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } }
Теперь мы успешно переместили элемент <header>
и всю основную логику из AppComponent
в его собственный TodoListHeaderComponent
.
TodoListHeaderComponent
является немым компонентом, и AppComponent
остается ответственным за хранение задачи с использованием TodoDataService
.
Далее, давайте рассмотрим TodoListComponent
.
Создание компонента TodoListComponent
Давайте снова используем Angular CLI для генерации нашего TodoListComponent
:
$ ng generate component todo-list
Это создает следующие файлы для нас:
create src/app/todo-list/todo-list.component.css create src/app/todo-list/todo-list.component.html create src/app/todo-list/todo-list.component.spec.ts create src/app/todo-list/todo-list.component.ts
Он также автоматически добавляет TodoListComponent
в объявления AppModule
:
// ... import { TodoListComponent } from './todo-list/todo-list.component'; @NgModule({ declarations: [ // ... TodoListComponent ], // ... }) export class AppModule { }
Теперь мы возьмем HTML, связанный со src/app/app.component.html
из src/app/app.component.html
:
<section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete"> <label>{{todo.title}}</label> <button class="destroy" (click)="removeTodo(todo)"></button> </div> </li> </ul> </section>
Мы также перемещаем его в src/app/todo-list/todo-list.component.html
:
<section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <app-todo-list-item [todo]="todo" (toggleComplete)="onToggleTodoComplete($event)" (remove)="onRemoveTodo($event)"></app-todo-list-item> </li> </ul> </section>
Обратите внимание, что мы ввели TodoListItemComponent
который еще не существует. Однако добавление его в шаблон уже позволяет нам изучить, какой API должен предлагать TodoListItemComponent
. Это облегчает нам написание TodoListItemComponent
в следующем разделе, потому что теперь мы знаем, какие входные и выходные данные мы ожидаем от TodoListItemComponent
.
Мы передаем элемент todo
через свойство todo
используя синтаксис входного свойства [todo]
и присоединяем обработчики событий к событиям, которые, как мы ожидаем, должны TodoListItemComponent
, например к событию toggleComplete
событию remove
.
Давайте откроем src/app/todo-list/todo-list.component.ts
и добавим логику, необходимую для нашего шаблона представления:
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Todo } from '../todo'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.css'] }) export class TodoListComponent { @Input() todos: Todo[]; @Output() remove: EventEmitter<Todo> = new EventEmitter(); @Output() toggleComplete: EventEmitter<Todo> = new EventEmitter(); constructor() { } onToggleTodoComplete(todo: Todo) { this.toggleComplete.emit(todo); } onRemoveTodo(todo: Todo) { this.remove.emit(todo); } }
Чтобы дополнительно продемонстрировать разницу между умными и немыми компонентами, мы также сделаем TodoListComponent
немым компонентом.
Сначала мы определяем @Input()
входного свойства , помечая его декоратором @Input()
. Это позволяет нам вводить todos
из родительского компонента.
Далее мы определяем два выходных события, remove
и toggleComplete
, используя декоратор @Output()
. Обратите внимание, как мы устанавливаем их тип в EventEmitter<Todo>
и назначаем им каждый новый экземпляр EventEmitter
.
Аннотация типа EventEmitter<Todo>
— это универсальный тип TypeScript, который сообщает TypeScript, что одновременно remove
и toggleComplete
являются экземплярами toggleComplete
и что значения, которые они toggleComplete
являются экземплярами Todo
.
Наконец, мы определяем обработчики событий onToggleTodoComplete(todo)
и onRemoveTodo(todo)
которые мы указали в нашем представлении, используя (toggleComplete)="onToggleTodoComplete($event)"
и (remove)="onRemoveTodo($event)"
.
Обратите внимание, как мы используем $event
качестве имени аргумента в шаблоне представления и todo
качестве имени параметра в определении метода. Чтобы получить доступ к полезной нагрузке (излучаемому значению) события в шаблоне Angular, мы всегда должны использовать $event
качестве имени аргумента.
Поэтому, указав (toggleComplete)="onToggleTodoComplete($event)"
в нашем шаблоне представления, мы сообщаем Angular использовать полезную нагрузку события в качестве первого аргумента при вызове метода onToggleTodoComplete
, который будет соответствовать первому параметру метода onToggleTodoComplete
, а именно todo
,
Мы знаем, что полезная нагрузка будет экземпляром todo
, поэтому мы определяем метод onToggleTodoComplete
как onToggleTodoComplete(todo: Todo)
, что облегчает чтение, понимание и сопровождение нашего кода.
Наконец, мы определяем наши обработчики событий, чтобы они также toggleComplete
событие toggleComplete
и remove
при получении входящей полезной нагрузки и определяем todo
в качестве полезной нагрузки события.
По сути, мы позволяем TodoListComponent
всплывать события из его дочерних экземпляров TodoListItemComponent
.
Это позволяет нам обрабатывать бизнес-логику вне TodoListComponent
, оставляя TodoListComponent
немым , гибким и легким.
Нам также нужно переименовать два метода в AppComponent
чтобы отразить это:
... export class AppComponent { // rename from toggleTodoComplete onToggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } // rename from removeTodo onRemoveTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } }
Если мы попытаемся запустить наше приложение на этом этапе, Angular выдаст ошибку:
Unhandled Promise rejection: Template parse errors: Can't bind to 'todo' since it isn't a known property of 'app-todo-list-item'. 1. If 'app-todo-list-item' is an Angular component and it has 'todo' input, then verify that it is part of this module. 2. If 'app-todo-list-item' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.
Это потому, что мы еще не создали TodoListItemComponent
.
Итак, давайте сделаем это дальше.
Создание компонента TodoListItemComponent
Опять же, мы используем Angular CLI для генерации нашего TodoListItemComponent
:
$ ng generate component todo-list-item
Это создает следующие файлы:
create src/app/todo-list-item/todo-list-item.component.css create src/app/todo-list-item/todo-list-item.component.html create src/app/todo-list-item/todo-list-item.component.spec.ts create src/app/todo-list-item/todo-list-item.component.ts
Он автоматически добавляет TodoListItemComponent
в объявления AppModule
:
// ... import { TodoListItemComponent } from './todo-list-item/todo-list-item.component'; @NgModule({ declarations: [ // ... TodoListItemComponent ], // ... }) export class AppModule { }
Теперь мы можем переместить оригинальную разметку из <li>
в src/app/todo-list-item.component.html
:
<div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete"> <label>{{todo.title}}</label> <button class="destroy" (click)="removeTodo(todo)"></button> </div>
Нам не нужно ничего менять в разметке, но мы должны убедиться, что события обрабатываются правильно, поэтому давайте добавим необходимый код нашего TodoListItemComponent
в src/app/todo-list-item/todo-list-item.component.ts
:
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Todo } from '../todo'; @Component({ selector: 'app-todo-list-item', templateUrl: './todo-list-item.component.html', styleUrls: ['./todo-list-item.component.css'] }) export class TodoListItemComponent { @Input() todo: Todo; @Output() remove: EventEmitter<Todo> = new EventEmitter(); @Output() toggleComplete: EventEmitter<Todo> = new EventEmitter(); constructor() { } toggleTodoComplete(todo: Todo) { this.toggleComplete.emit(todo); } removeTodo(todo: Todo) { this.remove.emit(todo); } }
Логика очень похожа на логику, которую мы имеем в TodoListComponent
.
Сначала мы определяем @Input()
чтобы мы могли передать экземпляр Todo
:
@Input() todo: Todo;
Затем мы определяем обработчики событий click для нашего шаблона и toggleComplete
событие toggleComplete
при нажатии флажка и событие remove
при нажатии toggleComplete
«X»:
@Output() remove: EventEmitter<Todo> = new EventEmitter(); @Output() toggleComplete: EventEmitter<Todo> = new EventEmitter(); toggleTodoComplete(todo: Todo) { this.toggleComplete.emit(todo); } removeTodo(todo: Todo) { this.remove.emit(todo); }
Обратите внимание, что мы на самом деле не обновляем и не удаляем данные. Мы просто TodoListItemComponent
события из TodoListItemComponent
когда пользователь щелкает ссылку для завершения или удаления TodoListItemComponent
, делая наш TodoListItemComponent
также немым компонентом.
Вспомните, как мы прикрепили обработчики событий к этим событиям в шаблоне TodoListComponent
:
<section class="main" *ngIf="todos.length > 0"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.complete"> <app-todo-list-item [todo]="todo" (toggleComplete)="onToggleTodoComplete($event)" (remove)="onRemoveTodo($event)"></app-todo-list-item> </li> </ul> </section>
Затем TodoListComponent
просто повторно генерирует события из TodoListItemComponent
.
TodoListItemComponent
события из TodoListItemComponent
через TodoListComponent
позволяют нам сохранять тупость обоих компонентов и гарантируют, что нам не придется обновлять их при рефакторинге TodoDataService
для связи с REST API в третьей части этой серии.
Как это круто!
Прежде чем двигаться дальше, давайте обновим наш шаблон AppComponent
чтобы использовать наш новый TodoListComponent
:
<section class="todoapp"> <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header> <!-- section is now replaced with app-todo-list --> <app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)" (remove)="onRemoveTodo($event)"></app-todo-list> <footer class="footer" *ngIf="todos.length > 0"> <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span> </footer> </section>
Наконец, давайте рассмотрим TodoListFooterComponent
.
Создание компонента TodoListFooterComponent
Опять же, из корня нашего проекта, мы используем Angular CLI для генерации TodoListFooterComponent
для нас:
$ ng generate component todo-list-footer
Это создает следующие файлы:
create src/app/todo-list-footer/todo-list-footer.component.css create src/app/todo-list-footer/todo-list-footer.component.html create src/app/todo-list-footer/todo-list-footer.component.spec.ts create src/app/todo-list-footer/todo-list-footer.component.ts
Он автоматически добавляет TodoListFooterComponent
в объявления AppModule
:
// ... import { TodoListFooterComponent } from './todo-list-footer/todo-list-footer.component'; @NgModule({ declarations: [ // ... TodoListFooterComponent ], // ... }) export class AppModule { }
Теперь мы перемещаем элемент <footer>
из src/app/app.component.html
в src/app/todo-list-footer/todo-list-footer.component.html
:
<footer class="footer" *ngIf="todos.length > 0"> <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span> </footer>
Мы также добавляем соответствующую логику в src/app/todo-list-footer/todo-list-footer.component.ts
:
import { Component, Input } from '@angular/core'; import { Todo } from '../todo'; @Component({ selector: 'app-todo-list-footer', templateUrl: './todo-list-footer.component.html', styleUrls: ['./todo-list-footer.component.css'] }) export class TodoListFooterComponent { @Input() todos: Todo[]; constructor() { } }
TodoListFooterComponent
не требует никаких методов. Мы определяем свойство todos
только с помощью декоратора @Input()
поэтому мы можем передавать todos, используя свойство todos
.
Наконец, давайте обновим наш шаблон AppComponent
чтобы также использовать наш новый TodoListFooterComponent
:
<section class="todoapp"> <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header> <app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)" (remove)="onRemoveTodo($event)"></app-todo-list> <app-todo-list-footer [todos]="todos"></app-todo-list-footer> </section>
Теперь мы успешно реорганизовали наш AppComponent
чтобы делегировать его функциональность TodoListHeaderComponent
, TodoListComponent
и TodoListFooterComponent
.
Прежде чем мы закончим эту статью, нам нужно сделать еще одно изменение.
Перемещение провайдера TodoDataService
В TodoDataService
части мы зарегистрировали TodoDataService
в качестве поставщика в AppComponent
:
import {Component} from '@angular/core'; import {Todo} from './todo'; import {TodoDataService} from './todo-data.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [TodoDataService] }) export class AppComponent { newTodo: Todo = new Todo(); constructor(private todoDataService: TodoDataService) { } addTodo() { this.todoDataService.addTodo(this.newTodo); this.newTodo = new Todo(); } toggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } }
Хотя это прекрасно работает для нашего приложения Todo, группа разработчиков Angular рекомендует добавить провайдеров всего приложения в корневой AppModule
вместо корневого AppComponent
.
Сервисы, зарегистрированные в AppComponent
, доступны только для AppComponent
и его дерева компонентов. Сервисы, зарегистрированные в AppModule
, доступны для всех компонентов всего приложения.
Если в какой-то момент наше приложение Todo вырастет и представит модули с TodoDataService
загрузкой, модули с TodoDataService
загрузкой не смогут получить доступ к TodoDataService
, поскольку TodoDataService
будет доступен только для AppComponent
и его дерева компонентов, а не внутри всего приложения.
Поэтому мы удаляем TodoDataService
в качестве поставщика в AppComponent
:
import {Component} from '@angular/core'; import {Todo} from './todo'; import {TodoDataService} from './todo-data.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [] }) export class AppComponent { newTodo: Todo = new Todo(); constructor(private todoDataService: TodoDataService) { } addTodo() { this.todoDataService.addTodo(this.newTodo); this.newTodo = new Todo(); } toggleTodoComplete(todo: Todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo: Todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } }
Затем добавьте его в качестве поставщика в AppModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { TodoDataService } from './todo-data.service'; import { TodoListComponent } from './todo-list/todo-list.component'; import { TodoListFooterComponent } from './todo-list-footer/todo-list-footer.component'; import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component'; import { TodoListItemComponent } from './todo-list-item/todo-list-item.component'; @NgModule({ declarations: [ AppComponent, TodoListComponent, TodoListFooterComponent, TodoListHeaderComponent, TodoListItemComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [TodoDataService], bootstrap: [AppComponent] }) export class AppModule { }
На этом заканчивается вторая часть этой серии.
Резюме
В первой статье мы узнали, как:
- инициализировать наше приложение Todo с помощью Angular CLI
- создать класс
Todo
для представления отдельных задач - создать сервис
TodoDataService
для создания, обновления и удаления задач - используйте компонент
AppComponent
для отображения пользовательского интерфейса - разверните наше приложение на страницах GitHub.
Во второй статье мы реорганизовали AppComponent
чтобы делегировать большую часть его работы:
-
TodoListComponent
для отображения списка задач -
TodoListItemComponent
для отображения одного todo -
TodoListHeaderComponent
для создания новой задачи -
TodoListFooterComponent
чтобы показать, сколькоTodoListFooterComponent
.
В процессе мы узнали:
- основы угловой компонентной архитектуры
- как передать данные в компонент, используя привязки свойств
- как прослушивать события, испускаемые компонентом, используя слушатели событий
- как разбиение компонентов на более мелкие повторно используемые компоненты облегчает повторное использование и поддержку нашего кода
- как мы можем использовать умные и глупые, чтобы сделать нашу жизнь намного проще, когда нам нужно реорганизовать бизнес-логику нашего приложения.
Весь код из этой статьи доступен по адресу https://github.com/sitepoint-editors/angular-todo-app .
В следующей части мы проведем рефакторинг TodoService
для связи с REST API.
Так что следите за обновлениями для третьей части!
Эта статья была рецензирована Vildan Softic . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
На онлайн-курсах Angular под руководством экспертов вы не можете пройти мимо Ultimate Angular от Todd Motto. Попробуйте его курсы здесь и используйте код SITEPOINT_SPECIAL, чтобы получить скидку 50% и помочь в поддержке SitePoint.