Angular 2.x is the simplest Angular 2 tutorial in history from 0 to 1 (4)

Keywords: Javascript angular JSON Attribute Mobile

Section one: Angular 2.0 from 0 to 1 (1)
The second section: Angular 2.0 from 0 to 1 (2)
The third section: Angular 2.0 from 0 to 1 (3)

Author: Wang Qian wpcfan@gmail.com

Section 4: Evolution! Modularize your application

Splitting a Complex Component

At the end of the last section, I lazily dumped a lot of code. Maybe you look a little dizzy. This is a typical function that will inevitably swell up after a period of accumulation of requirements. Now let's see how to split it up.
image_1b11kjibcelb6upnb21su41dilm.png-59.5kB
It seems that our application can be divided into Header, Main and Footer parts. First, let's create a new Component and type ng g c todo/todo-footer. Then cut the <footer> </footer> paragraph in src app todo todo. component. HTML into src app todo todo - footer todo - 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>
    <ul class="filters">
      <li><a href="">All</a></li>
      <li><a href="">Active</a></li>
      <li><a href="">Completed</a></li>
    </ul>
    <button class="clear-completed">Clear completed</button>
  </footer>

Looking at the above code, we can see that all variables seem to be todos?.length, which reminds us that for Footer, we don't need to pass in todos, we just need to give an item count. So let's change all todos?.length to itemCount.

<footer class="footer" *ngIf="itemCount > 0">
  <span class="todo-count">
    <strong>{{itemCount}}</strong> {{itemCount == 1 ? 'item' : 'items'}} left
  </span>
  <ul class="filters">
    <li><a href="">All</a></li>
    <li><a href="">Active</a></li>
    <li><a href="">Completed</a></li>
  </ul>
  <button class="clear-completed">Clear completed</button>
</footer>

That is to say, in src app todo todo todo. component. html, we can pass the todo item count to Footer with <app-todo-footer [itemCount]= "todos?. length"> </app-todo-footer>. So in src app todo todo. component. html, we just cut out the location of the code and add this sentence. Of course, if we want the parent component to pass values to the child component, we also need to declare it in the child component. @ Input() is a modifier for input binding, used to transfer data from parent components to child components.

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

@Component({
  selector: 'app-todo-footer',
  templateUrl: './todo-footer.component.html',
  styleUrls: ['./todo-footer.component.css']
})
export class TodoFooterComponent implements OnInit {
  //Declare that itemCount is an input value (from the reference)
  @Input() itemCount: number;
  constructor() { }
  ngOnInit() {
  }
}

Run it to see the effect. It should be all right!

Similarly, we create a Header component by typing ng g c todo/todo-header. Similarly, cut the following code from src app todo todo. component. HTML to src app todo todo-Header todo-Header. component. HTML

<header class="header">
  <h1>Todos</h1>
  <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
</header>

This code seems a bit cumbersome, mainly because it seems that we not only need to input something to the child component, but also want the child component to output something to the parent component, such as the value of the input box and the message of pressing the return key. Of course, you might have guessed that Angular 2 has @Input() in it and the corresponding @Output() modifier.
We want the placeholder text of the input box (default text displayed without input) to be an input parameter. When the return key is raised, an event can be sent to the parent component. At the same time, we also want the parent component to get the string when the input box enters text. That is to say, when a parent component invokes a child component, it looks like the following, which is equivalent to providing some events in our custom component. When a parent component invokes, it can write its own event handling method, and $event is the event object emitted by the child component:

<app-todo-header 
    placeholder="What do you want"
    (onTextChanges)="onTextChanges($event)"
    (onEnterUp)="addTodo()" >
</app-todo-header>

But the third requirement is that "the parent component can get the string when entering text in the input box", which is a bit problematic. If every character input is returned to the parent component, the system will communicate too frequently, and there may be performance problems. So we want to have a filter like this, which can filter out events in a certain period of time. So we define an input parameter delay.

<app-todo-header 
    placeholder="What do you want"
    delay="400"
    (textChanges)="onTextChanges($event)"
    (onEnterUp)="addTodo()" >
</app-todo-header>

Now the tag reference should look like the above, but we just planned what it looks like, we haven't done it yet. Let's start and see what we can do.
In the todo-header.component.html template, we adjust some variable names and parameters so that we don't confuse the child component's own template with the template fragments that refer to the child component in the parent component.

//todo-header.component.html
<header class="header">
  <h1>Todos</h1>
  <input
    class="new-todo"
    [placeholder]="placeholder"
    autofocus=""
    [(ngModel)]="inputValue"
    (keyup.enter)="enterUp()">
</header>

Keep in mind that the template for a child component is a description of what the child component looks like and what behavior it should behave, which has nothing to do with the parent component. For example, the placeholder in todo-header.component.html is an attribute in the HTML tag Input, which is not associated with the parent component. If we do not declare @Input() placeholder in todo-header.component.ts, then the child component does not have this attribute and cannot set it in the parent component. The attribute declared as @Input() in the parent component becomes the attribute visible to the child component. We can declare @Input() placeholder as @Input() hintText, so that when we refer to the header component, we need to write <app-todo-header hintText= "What do you want"...
Now take a look at todo-header.component.ts

import { Component, OnInit, Input, Output, EventEmitter, ElementRef } from '@angular/core';
import {Observable} from 'rxjs/Rx';
import 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

@Component({
  selector: 'app-todo-header',
  templateUrl: './todo-header.component.html',
  styleUrls: ['./todo-header.component.css']
})
export class TodoHeaderComponent implements OnInit {
  inputValue: string = '';
  @Input() placeholder: string = 'What needs to be done?';
  @Input() delay: number = 300;

  //detect the input value and output this to parent
  @Output() textChanges = new EventEmitter<string>();
  //detect the enter keyup event and output this to parent
  @Output() onEnterUp = new EventEmitter<boolean>();

  constructor(private elementRef: ElementRef) {
    const event$ = Observable.fromEvent(elementRef.nativeElement, 'keyup')
      .map(() => this.inputValue)
      .debounceTime(this.delay)
      .distinctUntilChanged();
    event$.subscribe(input => this.textChanges.emit(input));
  }
  ngOnInit() {
  }
  enterUp(){
    this.onEnterUp.emit(true);
    this.inputValue = '';
  }
}

Now let's analyze the code:
Placement holder and delay are two input variables, so these two attributes can be set in the < app-todo-header > tag.
Next we see onTextChanges and onEnterUp decorated with @Output, which, as the name implies, handle text changes and carriage return keylifts separately. Both variables are defined as Event Emitter. We emit the corresponding events in the logical code of the child component under the appropriate conditions, and the parent component receives these events. Here we use two methods to trigger the transmitter.

  • EnterUp: This is a more conventional method. In todo-header.component.html, we define (keyup.enter)="enterUp()", so in the component's enterUp method, we directly let onEnterUp emit corresponding events.

  • Rx in the constructor: There's a lot of new knowledge involved here. First, we injected ElementRef, which is a cautious object in Angular because it allows you to directly manipulate DOM, that is, HTML elements and events. At the same time, we use Rx (Responsive Object). Rx is a very complex topic. We don't expand it here, but we mainly use Observable to observe keyup events in HTML, then make a transformation in the event stream to send out the value of the input box (map), apply a debounce time filter, and then apply a distinct Unt filter (distinct Unt). IlChanged). Because the launch conditions of this event depend on the conditions at the time of input, we can't handle it as triggered by template events as before.
    Finally, we need to add the processing of the header output parameter launch event in todo.component.ts.

  onTextChanges(value) {
    this.desc = value;
  }

Finally, because after the component is split, we want to split the css as well, so here's the code
The style of todo-header.component.css is as follows:

h1 {
    position: absolute;
    top: -155px;
    width: 100%;
    font-size: 100px;
    font-weight: 100;
    text-align: center;
    color: rgba(175, 47, 47, 0.15);
    -webkit-text-rendering: optimizeLegibility;
    -moz-text-rendering: optimizeLegibility;
    text-rendering: optimizeLegibility;
}
input::-webkit-input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
input::-moz-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
input::input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
.new-todo,
.edit {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    font-family: inherit;
    font-weight: inherit;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    padding: 6px;
    border: 1px solid #999;
    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
.new-todo {
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}

todo-footer.component.cssThe style is as follows

.footer {
    color: #777;
    padding: 10px 15px;
    height: 20px;
    text-align: center;
    border-top: 1px solid #e6e6e6;
}
.footer:before {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 50px;
    overflow: hidden;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
                0 8px 0 -3px #f6f6f6,
                0 9px 1px -3px rgba(0, 0, 0, 0.2),
                0 16px 0 -6px #f6f6f6,
                0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
    float: left;
    text-align: left;
}
.todo-count strong {
    font-weight: 300;
}
.filters {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
}
.filters li {
    display: inline;
}
.filters li a {
    color: inherit;
    margin: 3px;
    padding: 3px 7px;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
}
.filters li a:hover {
    border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
    border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
    float: right;
    position: relative;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
}
.clear-completed:hover {
    text-decoration: underline;
}
@media (max-width: 430px) {
    .footer {
        height: 50px;
    }
    .filters {
        bottom: 10px;
    }
}

Of course, the above code should be deleted from todo.component.css. Now todo.component.css looks like this.

.todoapp {
    background: #fff;
    margin: 130px 0 40px 0;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
                0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.main {
    position: relative;
    z-index: 2;
    border-top: 1px solid #e6e6e6;
}
.todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
}
.todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
    border-bottom: none;
}
.todo-list li.editing {
    border-bottom: none;
    padding: 0;
}
.todo-list li.editing .edit {
    display: block;
    width: 506px;
    padding: 12px 16px;
    margin: 0 0 0 43px;
}
.todo-list li.editing .view {
    display: none;
}
.todo-list li .toggle {
    text-align: center;
    width: 40px;
    /* auto, since non-WebKit browsers doesn't support input styling */
    height: auto;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none; /* Mobile Safari */
    -webkit-appearance: none;
    appearance: none;
}
.todo-list li .toggle:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
    word-break: break-all;
    padding: 15px 60px 15px 15px;
    margin-left: 45px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
}
.todo-list li.completed label {
    color: #d9d9d9;
    text-decoration: line-through;
}
.todo-list li .destroy {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
    color: #af5b5e;
}
.todo-list li .destroy:after {
    content: '×';
}
.todo-list li:hover .destroy {
    display: block;
}
.todo-list li .edit {
    display: none;
}
.todo-list li.editing:last-child {
    margin-bottom: -1px;
}
label[for='toggle-all'] {
    display: none;
}
.toggle-all {
    position: absolute;
    top: -55px;
    left: -12px;
    width: 60px;
    height: 34px;
    text-align: center;
    border: none; /* Mobile Safari */
}
.toggle-all:before {
    content: '❯';
    font-size: 22px;
    color: #e6e6e6;
    padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
    color: #737373;
}
/*
    Hack to remove background from Mobile Safari.
    Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
    .toggle-all,
    .todo-list li .toggle {
        background: none;
    }
    .todo-list li .toggle {
        height: 40px;
    }
    .toggle-all {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
        -webkit-appearance: none;
        appearance: none;
    }
}

Encapsulation into independent modules

Now we have a lot of files in our todo directory, and we have observed that this function is relatively independent. In this case, it seems unnecessary to declare all components in the root module AppModule, because similar sub-components are not used elsewhere. Angular provides a way of organizing, that is, modules. Modules are very similar to root modules. First, we build a file src app todo todo. module. ts in the todo directory.

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';

import { routing} from './todo.routes'

import { TodoComponent } from './todo.component';
import { TodoFooterComponent } from './todo-footer/todo-footer.component';
import { TodoHeaderComponent } from './todo-header/todo-header.component';
import { TodoService } from './todo.service';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpModule,
    routing
  ],
  declarations: [
    TodoComponent,
    TodoFooterComponent,
    TodoHeaderComponent
  ],
  providers: [
    {provide: 'todoService', useClass: TodoService}
    ]
})
export class TodoModule {}

Note that instead of introducing BrowserModule, we introduced CommonModule. Importing BrowserModule will make all components, instructions and pipes exposed by the module directly available in any component template under AppModule without additional tedious steps. CommonModule provides instructions commonly used in many applications, including NgIf and NgFor. BrowserModule imports CommonModule and re-exports it. The final effect is that as long as BrowserModule is imported, instructions in CommonModule are automatically obtained. Almost all root modules of applications to be used in browsers should import Browser Modules from @angular/platform-browser. Do not import BrowserModule in any other module, instead import CommonModule. They need general instructions. They do not need to reinitialize full-application providers.
Because it's very similar to the root module, we won't expand on it. What you need to do is change the TodoService in TodoComponent to inject it with @Inject('todoService'). But notice that we need the module's own routing definition. We create a todo.routes.ts file under the todo directory, similar to that under the root directory.

import { Routes, RouterModule } from '@angular/router';
import { TodoComponent } from './todo.component';

export const routes: Routes = [
  {
    path: 'todo',
    component: TodoComponent
  }
];
export const routing = RouterModule.forChild(routes);

Here we only define a routing as "todo". Another difference from root routing is export const routing = RouterModule.forChild(routes); we use forChild instead of forRoot, because forRoot can only be used in the root directory, and all other modules that are not root modules can only use forChild for routing. Now we have to change the root route. src app app app. routes. TS looks like this:

import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'login',
    component: LoginComponent
  },
  {
    path: 'todo',
    redirectTo: 'todo'
  }
];
export const routing = RouterModule.forRoot(routes);

Notice that we removed the dependency of TodoComponent and changed the todo path to redirecTo to todo path, but did not give components. This is called "component-free routing". That is to say, TodoModule is responsible for the latter.
At this point, we can remove the Todo-related components referenced in AppModule.

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

import { TodoModule } from './todo/todo.module';

import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryTodoDbService } from './todo/todo-data';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';


@NgModule({
  declarations: [
    AppComponent,
    LoginComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryTodoDbService),
    routing,
    TodoModule
  ],
  providers: [
    {provide: 'auth',  useClass: AuthService}
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }

At this point, we noticed that nowhere else < app-todo > </app-todo > should be referenced, which means that we can safely delete selector:'app-todo', from the @Component modifier in the Todo component.

More authentic web Services

We don't want to use memory Web services anymore, because if we use them, we can't encapsulate them in TodoModule. So we use a more "real" web service: json-server. Install json-server using NPM install-g json-server. Then create todo-data.json under the todo directory

{
  "todos": [
    {
      "id": "f823b191-7799-438d-8d78-fcb1e468fc78",
      "desc": "blablabla",
      "completed": false
    },
    {
      "id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0",
      "desc": "getting up",
      "completed": false
    },
    {
      "id": "c1092224-4064-b921-77a9-3fc091fbbd87",
      "desc": "you wanna try",
      "completed": false
    },
    {
      "id": "e89d582b-1a90-a0f1-be07-623ddb29d55e",
      "desc": "have to say good",
      "completed": false
    }
  ]
}

Change in src app todo todo. service. TS

// private api_url = 'api/todos';
  private api_url = 'http://localhost:3000/todos';

And replace res.json().data in the then statement in addTodo and getTodos with res.json(). Delete statements related to memory web services in AppModule.

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

import { TodoModule } from './todo/todo.module';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';


@NgModule({
  declarations: [
    AppComponent,
    LoginComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing,
    TodoModule
  ],
  providers: [
    {provide: 'auth',  useClass: AuthService}
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }

In addition, open a command window, enter the project directory, and enter json-server. / src/app/todo/todo-data.json

Let's enjoy the results.
image_1b12b5v4onlm16ai1bdn7pu143e9.png-165.7kB

Perfecting Todo Application

Before concluding this section, we need to close the Todo application with some functions that are not yet complete:

  • Architecturally, it seems that we can further build TodoList and TodoItem components.

  • Full Selection and Inversion

  • Bottom filter: All, Active, Completed

  • Clean up completed projects

TodoItem and TodoList components

Type ng g c todo/todo-item in the command line window, and angular-cli will be very smart to help you build the TodoItem component in the todo directory and declare it in the TodoModule. Generally speaking, if you want to generate components under a module, enter the ng g c module name / component name. OK. Similarly, let's create a TodoList control, ng g c todo/todo-list. We hope todo.component.html will look like this in the future

//todo.component.html
<section class="todoapp">
  <app-todo-header
    placeholder="What do you want"
    (textChanges)="onTextChanges($event)"
    (onEnterUp)="addTodo()" >
  </app-todo-header>
  <app-todo-list
    [todos]="todos"
    (onRemoveTodo)="removeTodo($event)"
    (onToggleTodo)="toggleTodo($event)"
    >
  </app-todo-list>
  <app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
</section>

So where did TodoItem go? TodoItem is a sub-component of TodoList, and the template of TodoItem should be a todo template within the todos loop. The HTML template for TodoList should look like the following:

<section class="main" *ngIf="todos?.length > 0">
  <input class="toggle-all" type="checkbox">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.completed">
      <app-todo-item
        [isChecked]="todo.completed"
        (onToggleTriggered)="onToggleTriggered(todo)"
        (onRemoveTriggered)="onRemoveTriggered(todo)"
        [todoDesc]="todo.desc">
      </app-todo-item>
    </li>
  </ul>
</section>

So let's first look at the bottom of TodoItem, how does this component come apart? Let's start with todo-item.component.html

<div class="view">
  <input class="toggle" type="checkbox" (click)="toggle()" [checked]="isChecked">
  <label [class.labelcompleted]="isChecked" (click)="toggle()">{{todoDesc}}</label>
  <button class="destroy" (click)="remove(); $event.stopPropagation()"></button>
</div>

We need to determine which input and output parameters are available.

  • isChecked: Input parameter to determine whether it is selected, set by the parent component (TodoList)

  • todoDesc: An input parameter that displays the text description of Todo, set by the parent component

  • OnToggle Triggered: Output parameter that notifies the parent component as an event when the user clicks checkbox or label. In TodoItem, we emit this event in the toggle method when dealing with user click events.

  • onRemoveTriggered: Output parameter that notifies the parent component as an event when the user clicks the delete button. In TodoItem, we emit this event in the remove method when dealing with the user clicking the button event.

//todo-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-todo-item',
  templateUrl: './todo-item.component.html',
  styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent{
  @Input() isChecked: boolean = false;
  @Input() todoDesc: string = '';
  @Output() onToggleTriggered = new EventEmitter<boolean>();
  @Output() onRemoveTriggered = new EventEmitter<boolean>();

  toggle() {
    this.onToggleTriggered.emit(true);
  }
  remove() {
    this.onRemoveTriggered.emit(true);
  }
}

After we have established TodoItem, let's look at TodoList again, or at the template.

<section class="main" *ngIf="todos?.length > 0">
  <input class="toggle-all" type="checkbox">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.completed">
      <app-todo-item
        [isChecked]="todo.completed"
        (onToggleTriggered)="onToggleTriggered(todo)"
        (onRemoveTriggered)="onRemoveTriggered(todo)"
        [todoDesc]="todo.desc">
      </app-todo-item>
    </li>
  </ul>
</section>

TodoList requires an input parameter todos, specified by the parent component. TodoList itself does not need to know how the array came from. It and TodoItem are just responsible for displaying. Of course, because we also have TodoITem sub-components in TodoList, and TodoList itself does not handle this output parameter, we need to pass the output parameters of sub-components to TodoComponent for processing.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo.model';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
  _todos: Todo[] = [];
  @Input()
  set todos(todos:Todo[]){
    this._todos = [...todos];
  }
  get todos() {
    return this._todos;
  }
  @Output() onRemoveTodo = new EventEmitter<Todo>();
  @Output() onToggleTodo = new EventEmitter<Todo>();

  onRemoveTriggered(todo: Todo) {
    this.onRemoveTodo.emit(todo);
  }
  onToggleTriggered(todo: Todo) {
    this.onToggleTodo.emit(todo);
  }
}

There's a new thing in the code above, that is, before the todos() method, we see two access modifiers, set and get. This is because if we give todos as a member variable, after setting, if the todos array of the parent component changes, the child component does not know the change, and therefore cannot update the content of the child component itself. So we make a method of todos, and modify it into attribute method by get and set, that is to say, {todos} can be written if quoted from the template. By marking set todos() as @Input, we can monitor the data changes of the parent component.

Looking back at todo.component.html, we can see that (onRemoveTodo)="removeTodo($event)", which is used to process the on RemoveTodo of the sub-component (TodoList), and that $event is actually the parameter carried by the event reflector (todo:Todo here). We use this mechanism to complete data exchange between components.

//todo.component.html
<section class="todoapp">
  <app-todo-header
    placeholder="What do you want"
    (textChanges)="onTextChanges($event)"
    (onEnterUp)="addTodo()" >
  </app-todo-header>
  <app-todo-list
    [todos]="todos"
    (onRemoveTodo)="removeTodo($event)"
    (onToggleTodo)="toggleTodo($event)"
    >
  </app-todo-list>
  <app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
</section>

Speaking of this, you may want to ask whether it is over-designed, so few functions need such a design? Yes, this case is over-designed, but our goal is to show more Angular combat methods and features.

Fill in the pit to complete the missing function

Now we're missing a few features: Toggle All, Clear Completed, and status filters. Our design guideline is to put logical functions in TodoComponent, while other sub-components are only responsible for performance. In that case, let's first look at how it should be done logically.

Transfer data with routing parameters

First, look at the filters. In Footer, we have three filters: All, Active and Computed. Click on any filter and we just want to show the filtered data.

image_1b17mtibdkjn105l1ojl1dgr9il9.png-6.5kB

There are actually several ways to implement this function. The first is to set up an @Output event emitter according to the way that data is passed between components mentioned earlier. But in this section, we adopt another way, through routing parameters to achieve. Angular 2 can add parameters to routing. The simplest way is to use / todo as our Todo Component processing path. If you want to carry a filter parameter, you can write it in the routing definition.

  {
    path: 'todo/:filter',
    component: TodoComponent
  }

This: filter is a parameter expression, that is to say, todo/ACTIVE means the parameter filter='ACTIVE'. It looks a bit like sub-routing, but here we use a component to handle different paths, so the data behind todo / is treated as routing parameters. That's easier. Let's write down the paths that several filters point to in todo-footer.component.html. Note that there is a need to use Angular 2's unique route link instruction (routerLink).

  <ul class="filters">
    <li><a routerLink="/todo/ALL">All</a></li>
    <li><a routerLink="/todo/ACTIVE">Active</a></li>
    <li><a routerLink="/todo/COMPLETED">Completed</a></li>
  </ul>

Of course, we also need to add routing parameters to the routing array in todo.routes.ts.

  {
    path: 'todo/:filter',
    component: TodoComponent
  }

The definition of root routing also needs to be rewritten, because when the original todo does not have parameters, we can directly redirect to the todo module, but the existing parameters should be redirected to the default parameter is "ALL" path;

  {
    path: 'todo',
    redirectTo: 'todo/ALL'
  }

Now open todo.component.ts to see how to receive this parameter:

  1. Introduce routing objects import {Router, Activated Route, Params} from'@angular/router';

  2. Activated Route and Router are injected into the structure

  constructor(
    @Inject('todoService') private service,
    private route: ActivatedRoute,
    private router: Router) {}

Then add the following code in ngOnInit(), which is called in ngOnInit() if the general logic code needs to be called.

  ngOnInit() {
    this.route.params.forEach((params: Params) => {
      let filter = params['filter'];
      this.filterTodos(filter);
    });
  }

The return from this.route.params is an Observable, which contains the parameters passed. Of course, our example is very simple and has only one, which is the filter just defined. Of course, we need to add various filters to the component: call the processing method in the service and then operate on the todos array. The original getTodos method in the component is no longer useful. Delete it.

  filterTodos(filter: string): void{
    this.service
      .filterTodos(filter)
      .then(todos => this.todos = [...todos]);
  }

Finally, let's look at how we can implement this approach in todo.service.ts

  // GET /todos?completed=true/false
  filterTodos(filter: string): Promise<Todo[]> {
    switch(filter){
      case 'ACTIVE': return this.http
                        .get(`${this.api_url}?completed=false`)
                        .toPromise()
                        .then(res => res.json() as Todo[])
                        .catch(this.handleError);
      case 'COMPLETED': return this.http
                          .get(`${this.api_url}?completed=true`)
                          .toPromise()
                          .then(res => res.json() as Todo[])
                          .catch(this.handleError);
      default:
        return this.getTodos();
    }
  }

So far, we have achieved great success. Let's see the effect. Now enter http://localhost:4200/todo and look at the browser's address bar. See, the path is automatically changed to http://localhost:4200/todo/ALL. Our redirection defined in the following route is working!
image_1b17o06nv10ob13d6pb1f5613pnm.png-137.8kB
Now, try clicking on one of the todo s to change its completion status, and then clicking Active, we see not only that the path has changed, but that the data has been updated as we expected.
image_1b17o6qjlb31grg1o7edjm1q4l13.png-128kB

Batch modification and deletion

The functions of Toggle All and ClearCompleted are actually a process of batch modification and deletion.
Increase Clear Completed button event handling in todo-footer.component.html

<button class="clear-completed" (click)="onClick()">Clear completed</button>

Clear Completed is in Footer, so we need to add an output parameter onClear and onClick() event handling method to the Footer component

//todo-footer.component.ts
...
  @Output() onClear = new EventEmitter<boolean>();
  onClick(){
    this.onClear.emit(true);
  }
...

Similarly, Toggle All is in TodoList, so add click events to it in todo-list.component.html

<input class="toggle-all" type="checkbox" (click)="onToggleAllTriggered()">

Method of adding an output parameter onToggle All and onToggle All Triggered in todo-list.component.ts

  @Output() onToggleAll = new EventEmitter<boolean>();
  onToggleAllTriggered() {
    this.onToggleAll.emit(true);
  }

Add new attributes just declared in the parent component template and add attributes for app-todo-list and app-todo-footer in todo.component.html:

  ...
  <app-todo-list
    ...
    (onToggleAll)="toggleAll()"
    >
  </app-todo-list>
  <app-todo-footer
    ...
    (onClear)="clearCompleted()">
  </app-todo-footer>
  ...

Finally, the corresponding processing method is added to the parent component (todo.component.ts). The most intuitive approach is to loop arrays and execute existing toggleTodo(todo: Todo) and removeTodo(todo: Todo). Let's change todo.component.ts and add the following two methods:

  toggleAll(){
    this.todos.forEach(todo => this.toggleTodo(todo));
  }

  clearCompleted(){
    const todos = this.todos.filter(todo=> todo.completed===true);
    todos.forEach(todo => this.removeTodo(todo));
  }

Save it first and click on the bottom arrow icon on the left of the input box or the "Clear Completed" in the lower right corner to see the effect.
image_1b1c8if181tld15hlj531aasi8a9.png-140kB
Be accomplished! Wait, wait a minute. It doesn't seem right. Let's look back at the toggleAll and clearCompleted methods. The obvious problem with current implementations is that they are synchronized again (this.todos.forEach() is a synchronization method). If our processing logic is complex, the current implementations will result in UI not responding. But if we don't, what do we do with a series of asynchronous operations? Promise.all(iterable) copes with this situation. It is suitable to handle a series of Promises together until all Promises are processed (or reject ed when an exception occurs), and then return a Promise with all the return values.

let p1 = Promise.resolve(3);
let p2 = 1337;
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

But there is another problem. Our current toggle Todo (todo: Todo) and removeTodo(todo: Todo) do not return to Promise, so we also need a minor modification:

//todo.component.ts fragment
toggleTodo(todo: Todo): Promise<void> {
    const i = this.todos.indexOf(todo);
    return this.service
      .toggleTodo(todo)
      .then(t => {
        this.todos = [
          ...this.todos.slice(0,i),
          t,
          ...this.todos.slice(i+1)
          ];
        return null;
      });
  }
  removeTodo(todo: Todo): Promise<void>  {
    const i = this.todos.indexOf(todo);
    return this.service
      .deleteTodoById(todo.id)
      .then(()=> {
        this.todos = [
          ...this.todos.slice(0,i),
          ...this.todos.slice(i+1)
        ];
        return null;
      });
  }
  toggleAll(){
    Promise.all(this.todos.map(todo => this.toggleTodo(todo)));
  }
  clearCompleted(){
    const completed_todos = this.todos.filter(todo => todo.completed === true);
    const active_todos = this.todos.filter(todo => todo.completed === false);
    Promise.all(completed_todos.map(todo => this.service.deleteTodoById(todo.id)))
      .then(() => this.todos = [...active_todos]);
  }

Now try the effect again, everything should be normal. Of course, this version is still problematic, essentially calling toggleTodo and removeTodo in a loop, which will lead to multiple HTTP connections, so the best strategy should be to ask the server back-end classmates to add a batch API to us. But server-side programming is not part of this tutorial, so it's not going to start here. Just remember to reduce the number of HTTP requests and the size of data packets sent if you're in production. When it comes to reducing the size of HTTP interactive data, we can make some modifications to the toggleTodo method in todo.service.ts. The original put method was to upload the entire todo data, but in fact we only changed the todo.completed attribute. If your web api meets the REST standard, we can use the PATCH method of Http instead of the PUT method, which uploads only the changed data.

  // It was PUT /todos/:id before
  // But we will use PATCH /todos/:id instead
  // Because we don't want to waste the bytes those don't change
  toggleTodo(todo: Todo): Promise<Todo> {
    const url = `${this.api_url}/${todo.id}`;
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    return this.http
            .patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})
            .toPromise()
            .then(() => updatedTodo)
            .catch(this.handleError);
  }

Finally, all the sub-components of Todo are not actually using ngInit, so it is not necessary to implement the NgInit interface, and the ngInit method and related interface references can be removed.

This section code: https://github.com/wpcfan/awe...

Section one: Angular 2.0 from 0 to 1 (1)
The second section: Angular 2.0 from 0 to 1 (2)
The third section: Angular 2.0 from 0 to 1 (3)

Posted by Maugrim_The_Reaper on Mon, 08 Apr 2019 18:12:32 -0700