Angular 从0到1 (四)

作者: 接灰的电子产品 | 来源:发表于2016-11-12 21:08 被阅读9140次

    第一节:初识Angular-CLI
    第二节:登录组件的构建
    第三节:建立一个待办事项应用
    第四节:进化!模块化你的应用
    第五节:多用户版本的待办事项应用
    第六节:使用第三方样式库及模块优化用
    第七节:给组件带来活力
    Rx--隐藏在Angular 2.x中利剑
    Redux你的Angular 2应用
    第八节:查缺补漏大合集(上)
    第九节:查缺补漏大合集(下)

    作者:王芃 wpcfan@gmail.com

    第四节:进化!模块化你的应用

    一个复杂组件的分拆

    上一节的末尾我偷懒的甩出了大量代码,可能你看起来都有点晕了,这就是典型的一个功能经过一段时间的需求累积后,代码也不可避免的臃肿起来。现在我们看看怎么分拆一下吧。

    image_1b11kjibcelb6upnb21su41dilm.png-59.5kBimage_1b11kjibcelb6upnb21su41dilm.png-59.5kB
    我们的应用似乎可以分为Header,Main和Footer几部分。首先我们来建立一个新的Component,键入ng g c todo/todo-footer。然后将src\app\todo\todo.component.html中的<footer>...</footer>段落剪切到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>
    

    观察上面的代码,我们看到似乎所有的变量都是todos?.length,这提醒我们其实对于Footer来说,我们并不需要传入todos,而只需要给出一个item计数即可。那么我们来把所有的todos?.length改成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>
    

    这样的话也就是说如果在src\app\todo\todo.component.html中我们可以用<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>去传递todo项目计数给Footer即可。所以在src\app\todo\todo.component.html中刚才我们剪切掉代码的位置加上这句吧。当然,如果要让父组件可以传递值给子组件,我们还需要在子组件中声明一下。@Input()是输入型绑定的修饰符,用于把数据从父组件传到子组件。

    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 {
      //声明itemCount是可以一个可输入值(从引用者处)
      @Input() itemCount: number;
      constructor() { }
      ngOnInit() {
      }
    }
    

    运行一下看看效果,应该一切正常!

    类似的我们建立一个Header组件,键入ng g c todo/todo-header,同样的把下面的代码从src\app\todo\todo.component.html中剪切到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>
    

    这段代码看起来有点麻烦,主要原因是我们好像不但需要给子组件输入什么,而且希望子组件给父组件要输出一些东西,比如输入框的值和按下回车键的消息等。当然你可能猜到了,Angular2里面有@Input()就相应的有@Output()修饰符。
    我们希望输入框的占位文字(没有输入的情况下显示的默认文字)是一个输入型的参数,在回车键抬起时可以发射一个事件给父组件,同时我们也希望在输入框输入文字时父组件能够得到这个字符串。也就是说父组件调用子组件时看起来是下面的样子,相当于我们自定义的组件中提供一些事件,父组件调用时可以写自己的事件处理方法,而$event就是子组件发射的事件对象:

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

    但是第三个需求也就是“在输入框输入文字时父组件能够得到这个字符串”,这个有点问题,如果每输入一个字符都要回传给父组件的话,系统会过于频繁进行这种通信,有可能会有性能的问题。那么我们希望可以有一个类似滤波器的东东,它可以过滤掉一定时间内的事件。因此我们定义一个输入型参数delay。

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

    现在的标签引用应该是上面这个样子,但我们只是策划了它看起来是什么样子,还没有做呢。我们一起动手看看怎么做吧。
    todo-header.component.html的模板中我们调整了一些变量名和参数以便让大家不混淆子组件自己的模板和父组件中引用子组件的模板片段。

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

    记住子组件的模板是描述子组件自己长成什么样子,应该有哪些行为,这些东西和父组件没有任何关系。比如todo-header.component.html中的placeholder就是HTML标签Input中的一个属性,和父组件没有关联,如果我们不在todo-header.component.ts中声明@Input() placeholder,那么子组件就没有这个属性,在父组件中也无法设置这个属性。父组件中的声明为@Input()的属性才会成为子组件对外可见的属性,我们完全可以把@Input() placeholder声明为@Input() hintText,这样的话在引用header组件时,我们就需要这样写<app-todo-header hintText="What do you want" ...
    现在看一下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 = '';
      }
    }
    

    下面我们来分析一下代码:
    placeholder和delay作为2个输入型变量,这样<app-todo-header>标签中就可以设置这两个属性了。
    接下来我们看到了由@Output修饰的onTextChanges和onEnterUp,这两个顾名思义是分别处理文本变化和回车键抬起事件的,这两个变量呢都定义成了EventEmitter(事件发射器)。我们会在子组件的逻辑代码中以适当的条件去发射对应事件,而父组件会接收到这些事件。我们这里采用了2中方法来触发发射器

    • enterUp:这个是比较常规的方法,在todo-header.component.html中我们定义了(keyup.enter)="enterUp()",所以在组件的enterUp方法中,我们直接让onEnterUp发射了对应事件。
    • 构造器中使用Rx:这里涉及了很多新知识,首先我们注入了ElementRef,这个是一个Angular中需要谨慎使用的对象,因为它可以让你直接操作DOM,也就是HTML的元素和事件。同时我们使用了Rx(响应式对象),Rx是一个很复杂的话题,这里我们不展开了,但我们主要是利用Observable去观察HTML中的keyup事件,然后在这个事件流中做一个转换把输入框的值发射出来(map),应用一个时间的滤波器(debounceTime),然后应用一个筛选器(distinctUntilChanged)。这里由于这个事件的发射条件是依赖于输入时的当时条件,我们没有办法按前面的以模板事件触发做处理。
      最后需要在todo.component.ts中加入对header输出参数发射事件的处理
      onTextChanges(value) {
        this.desc = value;
      }
    

    最后由于组件分拆后,我们希望也分拆一下css,这里就直接给代码了
    todo-header.component.css的样式如下:

    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.css的样式如下

    .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;
        }
    }
    

    当然上述代码要从todo.component.css中删除,现在的todo.component.css看起来是这个样子

    .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;
        }
    }
    

    封装成独立模块

    现在我们的todo目录下好多文件了,而且我们观察到这个功能相对很独立。这种情况下我们似乎没有必要将所有的组件都声明在根模块AppModule当中,因为类似像子组件没有被其他地方用到。Angular中提供了一种组织方式,那就是模块。模块和根模块很类似,我们先在todo目录下建一个文件src\app\todo\todo.module.ts

    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 {}
    

    注意一点,我们没有引入BrowserModule,而是引入了CommonModule。导入 BrowserModule 会让该模块公开的所有组件、指令和管道在 AppModule 下的任何组件模板中直接可用,而不需要额外的繁琐步骤。CommonModule 提供了很多应用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 导入了 CommonModule 并且 重新导出 了它。 最终的效果是:只要导入 BrowserModule 就自动获得了 CommonModule 中的指令。几乎所有要在浏览器中使用的应用的 根模块 ( AppModule )都应该从 @angular/platform-browser 中导入 BrowserModule 。在其它任何模块中都 不要导入 BrowserModule,应该改成导入 CommonModule 。 它们需要通用的指令。它们不需要重新初始化全应用级的提供商。
    由于和根模块很类似,我们就不展开讲了。需要做的事情是把TodoComponent中的TodoService改成用@Inject('todoService')来注入。但是注意一点,我们需要模块自己的路由定义。我们在todo目录下建立一个todo.routes.ts的文件,和根目录下的类似。

    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);
    

    这里我们只定义了一个路由就是“todo”,另外一点和根路由不一样的是export const routing = RouterModule.forChild(routes);,我们用的是forChild而不是forRoot,因为forRoot只能用于根目录,所有非根模块的其他模块路由都只能用forChild。下面就得更改根路由了,src\app\app.routes.ts看起来是这个样子:

    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);
    

    注意到我们去掉了TodoComponent的依赖,而且更改todo路径定义为redirecTo到todo路径,但没有给出组件,这叫做“无组件路由”,也就是说后面的事情是TodoModule负责的。
    此时我们就可以去掉AppModule中引用的Todo相关的组件了。

    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 { }
    

    而且此时我们注意到其实没有任何一个地方目前还需引用<app-todo></app-todo>了,这就是说我们可以安全地把selector: 'app-todo',从Todo组件中的@Component修饰符中删除了。

    更真实的web服务

    这里我们不想再使用内存Web服务了,因为如果使用,我们无法将其封装在TodoModule中。所以我们使用一个更“真”的web服务:json-server。使用npm install -g json-server安装json-server。然后在todo目录下建立todo-data.json

    {
      "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
        }
      ]
    }
    

    src\app\todo\todo.service.ts中更改

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

    并将addTodo和getTodos中then语句中的 res.json().data替换成res.json()。在AppModule中删掉内存web服务相关的语句。

    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 { }
    

    另外打开一个命令窗口,进入工程目录,输入json-server ./src/app/todo/todo-data.json

    欣赏一下成果吧


    image_1b12b5v4onlm16ai1bdn7pu143e9.png-165.7kBimage_1b12b5v4onlm16ai1bdn7pu143e9.png-165.7kB

    完善Todo应用

    在结束本节前,我们得给Todo应用收个尾,还差一些功能没完成:

    • 从架构上来讲,我们似乎还可以进一步构建出TodoList和TodoItem两个组件
    • 全选并反转状态
    • 底部筛选器:All,Active,Completed
    • 清理已完成项目

    TodoItem和TodoList组件

    在命令行窗口键入ng g c todo/todo-item,angular-cli会十分聪明的帮你在todo目录下建好TodoItem组件,并且在TodoModule中声明。一般来说,如果要生成某个模块下的组件,输入ng g c 模块名称/组件名称。 好的,类似的我们再建立一个TodoList控件,ng g c todo/todo-list。我们希望未来的todo.component.html是下面这个样子的

    //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>
    

    那么TodoItem哪儿去了呢?TodoItem是TodoList的子组件,TodoItem的模板应该是todos循环内的一个todo的模板。TodoList的HTML模板看起来应该是下面的样子:

    <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>
    

    那么我们先从最底层的TodoItem看,这个组件怎么剥离出来?首先来看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>
    

    我们需要确定有哪些输入型和输出型参数

    • isChecked:输入型参数,用来确定是否被选中,由父组件(TodoList)设置
    • todoDesc:输入型参数,显示Todo的文本描述,由父组件设置
    • onToggleTriggered:输出型参数,在用户点击checkbox或label时以事件形式通知父组件。在TodoItem中我们是在处理用户点击事件时在toggle方法中发射这个事件。
    • onRemoveTriggered:输出型参数,在用户点击删除按钮时以事件形式通知父组件。在TodoItem中我们是在处理用户点击按钮事件时在remove方法中发射这个事件。
    //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);
      }
    }
    

    建立好TodoItem后,我们再来看TodoList,还是从模板看一下

    <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需要一个输入型参数todos,由父组件(TodoComponent)指定,TodoList本身不需要知道这个数组是怎么来的,它和TodoItem只是负责显示而已。当然我们由于在TodoList里面还有TodoITem子组件,而且TodoList本身不会处理这个输出型参数,所以我们需要把子组件的输出型参数再传递给TodoComponent进行处理。

    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);
      }
    }
    

    上面代码中有一个新东东,就是在todos()方法前我们看到有setget两个访问修饰符。这个是由于我们如果把todos当成一个成员变量给出的话,在设置后如果父组件的todos数组改变了,子组件并不知道这个变化,从而不能更新子组件本身的内容。所以我们把todos做成了方法,而且通过get和set修饰成属性方法,也就是说从模板中引用的话可以写成{{todos}}。通过标记set todos()@Input我们可以监视父组件的数据变化。

    现在回过头来看一下todo.component.html,我们看到(onRemoveTodo)="removeTodo($event)",这句是为了处理子组件(TodoList)的输出型参数(onRemoveTodo),而$event其实就是这个事件反射器携带的参数(这里是todo:Todo)。我们通过这种机制完成组件间的数据交换。

    //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>
    

    讲到这里大家可能要问是不是过度设计了,这么少的功能用得着这么设计吗?是的,本案例属于过度设计,但我们的目的是展示出更多的Angular实战方法和特性。

    填坑,完成漏掉的功能

    现在我们还差几个功能:全部反转状态(ToggleAll),清除全部已完成任务(Clear Completed)和状态筛选器。我们的设计方针是逻辑功能放在TodoComponent中,而其他子组件只负责表现。这样的话,我们先来看看逻辑上应该怎么完成。

    用路由参数传递数据

    首先看一下过滤器,在Footer中我们有三个过滤器:All,Active和Completed,点击任何一个过滤器,我们只想显示过滤后的数据。

    image_1b17mtibdkjn105l1ojl1dgr9il9.png-6.5kBimage_1b17mtibdkjn105l1ojl1dgr9il9.png-6.5kB

    这个功能其实有几种可以实现的方式,第一种我们可以按照之前讲过的组件间传递数据的方式设置一个@Output的事件发射器来实现。但本节中我们采用另一种方式,通过路由传递参数来实现。Angular2可以给路由添加参数,最简单的一种方式是比如/todo是我们的TodoComponent处理的路径,如果希望携带一个filter参数的话,可以在路由定义中写成

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

    这个:filter是一个参数表达式,也就是说例如todo/ACTIVE就意味着参数filter='ACTIVE'。看上去有点像子路由,但这里我们使用一个组件去处理不同路径的,所以todo/后面的数据就被当作路由参数来对待了。这样的话就比较简单了,我们在todo-footer.component.html中把几个过滤器指向的路径写一下,注意这里和需要使用Angular2特有的路由链接指令(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>
    

    当然我们还需要在todo.routes.ts中增加路由参数到路由数组中

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

    根路由定义也需要改写一下,因为原来todo不带参数时,我们直接重定向到todo模块即可,但现在有参数的话应该重定向到默认参数是“ALL”的路径;

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

    现在打开todo.component.ts看看怎么接收这个参数:

    1. 引入路由对象 import { Router, ActivatedRoute, Params } from '@angular/router';
    2. 在构造中注入ActivatedRouteRouter
      constructor(
        @Inject('todoService') private service,
        private route: ActivatedRoute,
        private router: Router) {}
    

    然后在ngOnInit()中添加下面的代码,一般的逻辑代码如果需要在ngOnInit()中调用。

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

    this.route.params返回的是一个Observable,里面包含着所传递的参数,当然我们这个例子很简单只有一个,就是刚才定义的filter。当然我们需要在组件内添加对各种filter处理的方法:调用service中的处理方法后对todos数组进行操作。组件中原有的getTodos方法已经没有用了,删掉吧。

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

    最后我们看看在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();
        }
      }
    

    至此大功告成,我们来看看效果吧。现在输入http://localhost:4200/todo进入后观察浏览器地址栏,看到了吧,路径自动被修改成了http://localhost:4200/todo/ALL,我们的在跟路由中定义的重定向起作用了!

    image_1b17o06nv10ob13d6pb1f5613pnm.png-137.8kBimage_1b17o06nv10ob13d6pb1f5613pnm.png-137.8kB
    现在,试着点击其中某个todo更改其完成状态,然后点击Active,我们看到不光路径变了,数据也按照我们期待的方式更新了。
    image_1b17o6qjlb31grg1o7edjm1q4l13.png-128kBimage_1b17o6qjlb31grg1o7edjm1q4l13.png-128kB

    批量修改和批量删除

    ToggleAll和ClearCompleted的功能其实是一个批量修改和批量删除的过程。
    todo-footer.component.html中增加Clear Completed按钮的事件处理

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

    Clear Completed在Footer中,所以我们需要给Footer组件增加一个输出型参数onClearonClick()事件处理方法

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

    类似的,ToggleAll位于TodoList中,所以在todo-list.component.html中为其增加点击事件

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

    todo-list.component.ts中增加一个输出型参数onToggleAll和onToggleAllTriggered的方法

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

    在父组件模板中添加子组件中刚刚声明的新属性,在todo.component.html中为app-todo-list和app-todo-footer添加属性:

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

    最后在父组件(todo.component.ts)中添加对应的处理方法。最直觉的做法是循环数组,执行已有的toggleTodo(todo: Todo)removeTodo(todo: Todo)。我们更改一下todo.component.ts,增加下面两个方法:

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

    先保存一下,点击一下输入框左边的下箭头图标或者右下角的“Clear Completed”,看看效果

    image_1b1c8if181tld15hlj531aasi8a9.png-140kBimage_1b1c8if181tld15hlj531aasi8a9.png-140kB
    大功告成!慢着,等一下,哪里好像不太对。让我们回过头再看看toggleAll方法和clearCompleted方法。目前的实现方式有个明显问题,那就是现在的处理方式又变成同步的了(this.todos.forEach()是个同步方法),如果我们的处理逻辑比较复杂的话,现在的实现方式会导致UI没有响应。但是如果不这么做的话,对于一系列的异步操作我们怎么处理呢?Promise.all(iterable)就是应对这种情况的,它适合把一系列的Promise一起处理,直到所有的Promise都处理完(或者是异常时reject),之后也返回一个Promise,里面是所有的返回值。
    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"] 
    });
    

    但是还有个问题,我们目前的toggleTodo(todo: Todo)removeTodo(todo: Todo)并不返回Promise,所以也需要小改造一下:

    //todo.component.ts片段
    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]);
      }
    

    现在再去试试效果,应该一切功能正常。当然这个版本其实还是有问题的,本质上还是在循环调用toggleTodoremoveTodo,这样做会导致多次进行HTTP连接,所以最佳策略应该是请服务器后端同学增加一个批处理的API给我们。但是服务器端的编程不是本教程的范畴,这里就不展开了,大家只需记住如果在生产环境中切记要减少HTTP请求的次数和缩减发送数据包的大小。说到减小HTTP交互数据的大小的话,我们的todo.service.ts中可以对toggleTodo方法做点改造。原来的put方法是将整个todo数据上传,但其实我们只改动了todo.completed属性。如果你的web api是符合REST标准的话,我们可以用Http的PATCH方法而不是PUT方法,PATCH方法会只上传变化的数据。

      // 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);
      }
    

    最后其实Todo的所有子组件其实都没有用到ngInit,所以不必实现NgInit接口,可以去掉ngInit方法和相关的接口引用。

    本节代码: https://github.com/wpcfan/awesome-tutorials/tree/chap04/angular2/ng2-tut

    慕课网 Angular 视频课上线: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner

    纸书出版了,比网上内容丰富充实了,欢迎大家订购!
    京东链接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

    Angular从零到一Angular从零到一

    第一节:初识Angular-CLI
    第二节:登录组件的构建
    第三节:建立一个待办事项应用
    第四节:进化!模块化你的应用
    第五节:多用户版本的待办事项应用
    第六节:使用第三方样式库及模块优化用
    第七节:给组件带来活力
    Rx--隐藏在Angular 2.x中利剑
    Redux你的Angular 2应用
    第八节:查缺补漏大合集(上)
    第九节:查缺补漏大合集(下)

    相关文章

      网友评论

      • 127ea2035baf:我根据这个一步一操作,但是在完善todo应用的时候,有两个报错
        ERROR
        TypeError: Cannot read property 'slice' of undefined
        at TodoListComponent.Object.defineProperty.set [as todos] (webpack-internal:///../../../../../src/app/todo/todo-list/todo-list.component.ts:24:32)
        at updateProp (webpack-internal:///../../../core/esm5/core.js:12830:37)
        at checkAndUpdateDirectiveInline (webpack-internal:///../../../core/esm5/core.js:12537:19)
        at checkAndUpdateNodeInline (webpack-internal:///../../../core/esm5/core.js:14104:20)
        at checkAndUpdateNode (webpack-internal:///../../../core/esm5/core.js:14047:16)
        at debugCheckAndUpdateNode (webpack-internal:///../../../core/esm5/core.js:14940:76)
        at debugCheckDirectivesFn (webpack-internal:///../../../core/esm5/core.js:14881:13)
        at Object.eval [as updateDirectives] (ng:///TodoModule/TodoComponent.ngfactory.js:54:9)
        at Object.debugUpdateDirectives [as updateDirectives] (webpack-internal:///../../../core/esm5/core.js:14866:21)
        at checkAndUpdateView (webpack-internal:///../../../core/esm5/core.js:14013:14)View_TodoComponent_0 @ TodoComponent.html:2
        TodoComponent.html:2 ERROR CONTEXT DebugContext_View_TodoComponent_0 @ TodoComponent.html:2
        127ea2035baf:@lovelqy20 找到问题了,也和大家分享下
        分析原因:@Input()
        set todos(todos:Todo[]){
        this._todos = [...todos];
        }
        此时todos必须初始化,否则第一次加载的时候是undefined,此时就会报上述错误。
        解决方案:
        1、在todo.component.ts父组件中,初始化todo ,todos: Todo[]=[];即可
        2、不采用set,get修饰符的方式,直接 @Input() todos: Todo[] = []
        127ea2035baf:代码提示错误就是在set修饰符的地方。
      • 闪沙:<footer class="footer" *ngIf="todos?.length > 0">,是不是写错了?todos?.length中的问号是什么意思?
      • 27caacba94dd:请问一下老师,为什么todo-footer子组件传递的itemCount必须用[]包裹,这个我能理解,是单向数据传递;那todo-header里的placeholder又不能用[]呢,同样是用@Input修饰,有什么不同的地方吗?
        27caacba94dd:@LY_b764 谢谢
        f0cdf45dd48e:用[]包裹是因为它右边其实是一个插值表达式,你也可以把[itemCount]="todos?.length"写成itemCout="{{todos?.length}}",而placeholder="What do you want"是固定的值,所以不一样
      • wakaZ:老师不好意思问下,我按照您的教程做下来发现我的footer无法显示,我在程序加了些console.log然后发现为什么是先运行footer的加载再运行list数据的取得,这样的话不就取得不到todos的长度了么?
      • 13e1aa0cccfa:您好,大叔,我按照您的教程一步步写过来。不论我对页面进行任何,toggle,add,delete都会导致程序重新被编译一次,没有任何报错
        13e1aa0cccfa:@接灰的电子产品 嗯嗯,发现了,谢谢大叔:+1:
        13e1aa0cccfa:哦,我知道了,因为todo-data.json也在项目目录里,所以有改变的话整个项目就会被重新编译
        接灰的电子产品:@墨韵留香_ 把数据文件 data.json 移到和src平级的目录即可
      • dream幻enjoy:请问,这里用json-server为什么不会有跨域问题啊...
        dream幻enjoy:@接灰的电子产品 我之前遇到了跨域问题,结果是"http://&quot;忘记加了直接localhost:3000了
        接灰的电子产品:因为它支持跨域啊:stuck_out_tongue_winking_eye:
      • neilyoTracer:老师,我还是没搞懂为什么TodoListComponent里面的todos要用set和get方法啊,而且_todos不是用private吗?
        接灰的电子产品:@neilyoTracer 就是告诉你可以这么用而已,你当然也可以直接 public _todos,这样不用 get set,但有的时候你可能不希望外部直接访问成员变量,这个时候可以利用 get set
      • 1f814c91db10:ERROR in ./~/selenium-webdriver/io/exec.js
        出现这个怎么解决 网上找不到中文的 英文的标识看不懂
      • 1f814c91db10:大神 还有 我想 问一下 import{service} + providers【service】 和@Inject(service)的区别在哪里 额 你知道我的意思吗?
        接灰的电子产品:@lengfeng_2ea3 看一下我的关于依赖注入那篇文章
      • 1f814c91db10:大神 我按照你的编写到更真实的web上面,然后运行,出来的是 两个框 和login 感觉是不是少了什么 导致没有 header 和footer那一部分
      • peaceforgdxxxzk:大神,为什么我的todo-footer-component显示不出来呢?
      • 兜兜转转的小菊:我照着老师写的 {provide: 'userService', useClass: UserService},引用 @Inject('userService') private userServ, 可是会报错误如下:
        ERROR Error: Uncaught (in promise): Error: No provider for UserService!
        Error
        at injectionError (core.es5.js:1231) [angular]
        at noProviderError (core.es5.js:1269) [angular]
        at ReflectiveInjector_._throwOrNull (core.es5.js:2770) [angular]
        at ReflectiveInjector_._getByKeyDefault (core.es5.js:2809) [angular]
        at ReflectiveInjector_._getByKey (core.es5.js:2741) [angular]
        at ReflectiveInjector_.get (core.es5.js:2610) [angular]
        at AppModuleInjector.get (module.ngfactory.js [sm]:1) [angular]
        at AppModuleInjector.getInternal (module.ngfactory.js [sm]:1) [angular]
        at AppModuleInjector.NgModuleInjector.get (core.es5.js:3556) [angular]
        at PagesModuleInjector.NgModuleInjector.get (core.es5.js:3557) [angular]
        at PreActivation.getToken (router.es5.js:4436) [angular]
        ...
        接灰的电子产品:@兜兜转转的小菊 奇怪,不应该啊,断掉原来的ng serve重新ng serve也不行吗?
        兜兜转转的小菊:@接灰的电子产品 在app.module.ts中 的 @NgModule({
        declarations: [
        ...widgets,
        ...pages,
        ],
        imports: [
        ...modules,
        ],
        providers: [
        ...services,
        {provide: 'userService', useClass: UserService},
        ],
        bootstrap: [AppComponent],
        })
        接灰的电子产品:@兜兜转转的小菊 你的provide是在什么地方呢
      • bbc1f74273b2:怎么总是自动刷新,连输入的数据都看不到就刷新了,这是怎么回事呀?
        接灰的电子产品:@森林中的风 json-server和刷新有啥关系
        bbc1f74273b2:亲 就是因为没有报异常 我才有点不知所措了 我是按照您的代码一点一点做下来的 就是做到这个加入json.server 这就 一输入完按了enter键 就刷新了
        接灰的电子产品:@森林中的风 亲,要学会问问题,这样的问题我根本没法回答。起码要看看console中和chrome中有什么异常信息
      • 阿里巴巴_8dc7:为什么我到《封装成独立模块》这步,在浏览器输入http://localhost:4200/todo,就出现错误。报错的消息是
        EXCEPTION: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'todo'
        Error: Cannot match any routes. URL Segment: 'todo'
        以下是工具包的版本信息
        "private": true,
        "dependencies": {
        "@angular/common": "^2.3.1",
        "@angular/compiler": "^2.3.1",
        "@angular/core": "^2.3.1",
        "@angular/forms": "^2.3.1",
        "@angular/http": "^2.3.1",
        "@angular/platform-browser": "^2.3.1",
        "@angular/platform-browser-dynamic": "^2.3.1",
        "@angular/router": "^3.3.1",
        "angular-in-memory-web-api": "^0.2.4",
        "angular2-uuid": "^1.1.1",
        "core-js": "^2.4.1",
        "rxjs": "^5.0.1",
        "ts-helpers": "^1.1.1",
        "zone.js": "^0.7.2"
        },
        "devDependencies": {
        "@angular/compiler-cli": "^2.3.1",
        "@types/jasmine": "2.5.38",
        "@types/node": "^6.0.42",
        "angular-cli": "1.0.0-beta.28.3",
        "codelyzer": "~2.0.0-beta.1",
        "jasmine-core": "2.5.2",
        "jasmine-spec-reporter": "2.5.0",
        "karma": "1.2.0",
        "karma-chrome-launcher": "^2.0.0",
        "karma-cli": "^1.0.1",
        "karma-jasmine": "^1.0.2",
        "karma-remap-istanbul": "^0.2.1",
        "protractor": "~4.0.13",
        "ts-node": "1.2.1",
        "tslint": "^4.3.0",
        "typescript": "~2.0.3"
        }
        }
        阿里巴巴_8dc7:@接灰的电子产品 检查出来了,原来在app.Module里还要导入todo模块,一直以为不需要导入:sweat:
        接灰的电子产品:@阿里巴巴_8dc7 路由错误,/todo没有被正确解析,看一下我github代码对比一下
      • 接灰的电子产品:@suzu 另外一个是子组件如果要复用的话最好与业务无关,否则无法复用
        接灰的电子产品:@接灰的电子产品 不是说不可以有,而是组件的目的在于复用,要起到复用的目的。
        接灰的电子产品:@suzu 这个你理解有问题,如果只是业务拆分的话何必组件?比如input有业务吗?
        aa83579ae189:子组件如果没有自己的业务,那何必要子组件
      • aa83579ae189:todo-item的事件为什么要一步步丢到todo.component去实现,自己组件实现自己的事件不好吗,否则为何要把todo.component拆成那么多组件
        aa83579ae189:@接灰的电子产品 从这方面讲是没有问题的,可能是大家对组件的设计理念不同。我比较倾向于按照业务来划分组件。
        接灰的电子产品:@suzu 另外一个是子组件如果要复用的话最好与业务无关,否则无法复用
        接灰的电子产品:@suzu 文章里说了有点小题大做,但是是为了展示组件内通讯和组件封装
      • 97c8870576cc:还有个问题,我是从本章一步一步按照这个教程来写的代码,
        然后在做到 封装成独立模块 这一章之前的代码时候,此时 footer已经完成ok,
        但是当我在做header的时候发现,当我输入文字后,新增加的行仅仅为空白行,我看了下angury里面的headerComponent发现inputValue已经被正确双向绑定了
        但我找来找去都没有发现
        在 todo-header.component.ts 里面的textChanges这个Output的赋值、
        在上面代码中我仅仅发现 event$.subscribe(input=>this.textChanges.emit(input)); 这个语句,其中是否缺少一个 textChanges -> this.inputValue 这样的一个赋值语句呢?

        谢谢

        97c8870576cc:@接灰的电子产品 对了,还有一事,
        @output() textChanges = new EventEmitter<string>();
        这句表达式可以不可以用DI来书写呢?
        接灰的电子产品:@老子牛逼 不难理解就跟你处理click事件一个道理,子组件暴露了一个事件给你,你去处理
        97c8870576cc:@老子牛逼 抱歉,是我写错了一条代码,也把自己的错误写出来给大家分享下

        <app-todo-header 中的 (textChanges)="onTextChange($event)" 这条的 textChanges 是子组件的 output
        而 onTextChange 则是父系组件的 method
        而我把这个写成了(onTextChanges)="onTextChange($event),也就是说,父组件一直没有接收到子组件的output,所以也就一直为空、、、、大意了
        但是这个中间的等号还是有点难理解,子output =父method 。。。。。
      • Evasi0n:怎么会一直自动刷新页面呢?
        Evasi0n:@接灰的电子产品 好的,谢谢您的教程
        接灰的电子产品:@Evasi0n debug模式下会这样,发布到生产环境就好了
      • Gooiem:业务需求决定了你的模块是否在其他地方能否被附用

      本文标题:Angular 从0到1 (四)

      本文链接:https://www.haomeiwen.com/subject/rhwxpttx.html