第一个 Angular 项目 - 动态页面

第一个 Angular 项目 - 动态页面

使用的所有技巧都在下面的笔记里:


静态页面的实现在这里:第一个 Angular 项目 - 静态页面

这篇笔记结合新学的绑定知识,盘活数据

拟态路由

Angular 有对 routing 的 built-in 支持,鉴于这里暂时还没有学到 routing,所以这里用的是拟态的方式去实现。这里的需求是 3 个点:

  1. 导航栏有一个对所处的页面进行高亮

  2. 切换到 recipe 页面只显示 recipe 的内容

  3. 切换到 shopping-list 页面只显示 shopping list 相关的内容

接下来对功能的实现进行分析:

  • 需要在 header 的 VM 层中保存一个变量,这个变量名用以动态修改 ngClass,并在所处的页面添加 active 这一 class 名

  • header 的 VM 层需要使用 @Output 这个指令去创建一个 EventEmitter,传输的对象为当前所处的页面

  • app 的 V 层和 VM 层需要动态接受从子组件传来的事件,并且动态渲染对应页面的内容

    动态渲染的部分可以用 ngIf 或是 ngSwitch 实现

分析完了后就可以开始实现了

header

header V 层

这里的修改相对比较简单,主要是 ngClass 的修改,绑定 click 事件这两个,这部分的内容都可以在 [Angular 基础] - 指令(directives) 中查看,修改如下:

<ul class="nav navbar-nav">
  <li [ngClass]="{ active: activeTab === 'recipe' }">
    <a href="#" (click)="onClickTab('recipe')">Recipes</a>
  </li>
  <li [ngClass]="{ active: activeTab === 'shopping-list' }">
    <a href="#" (click)="onClickTab('shopping-list')">Shopping List</a>
  </li>
</ul>
header VM 层

这里主要修改的部分就是:

  • 新建一个变量保存 activeTab
  • 新建一个 EventEmitter 向父组件传输当前的 activeTab
  • 新建一个 clickHandler 去实现上面两个操作

具体代码如下:

export class HeaderComponent {
  @Output() activeTabChanged = new EventEmitter<string>();
  activeTab: string = 'recipe';

  onClickTab(activeTab: string) {
    this.activeTab = activeTab;
    this.activeTabChanged.emit(activeTab);
  }
}

EventEmitter 部分的实现可以查看这篇笔记:[Angular 基础] - 自定义事件 & 自定义属性

app 路由修改

app 路由 V 层修改

V 层的修改相对比较简单,主要是需要将 EventEmitter 绑定一下,并且实现一下条件控制,我这里用 ngSwitch 实现:

<app-header (activeTabChanged)="onTabChange($event)"></app-header>

<div class="container">
  <div class="row">
    <div class="col-md-12">
      <div [ngSwitch]="activeTab">
        <app-recipes *ngSwitchCase="'recipe'"></app-recipes>
        <app-shopping-list *ngSwitchCase="'shopping-list'"></app-shopping-list>
      </div>
    </div>
  </div>
</div>
app 路由 VM 层修改

VM 层实现的功能也差不多,新建一个变量保存 activeTab,随后实现对应的 onTabChange 去修改 activeTab,实现如下:

export class AppComponent {
  title = 'recipe-book';
  activeTab = 'recipe';

  onTabChange($event: string) {
    this.activeTab = $event;
  }
}

最后实现的效果如下:

在这里插入图片描述

recipe-list 清理

recipe-list 部分要清理的比较多:

  • 将单独的 recipe 从 recipe-list 传到 recipe-item 中

    刚开始写的时候还没有涉及到组件之间的沟通,因此子组件无法获取父组件的数据

    现在父子组件已经可以沟通了,那么还是要遵从一下 SRP 的

  • 点击列表中的 recipe,右侧能够显示出 recipe 的具体信息

    这部分就像 wireframe 中的设定一样:

    在这里插入图片描述

传递 recipe 到子组件

recipe-list V 层清理
<a href="#" class="list-group-item clearfix" *ngFor="let recipe of recipes">
  <app-recipe-item [recipe]="recipe"></app-recipe-item>
</a>

这里原本的内容,包括渲染名称、描述、图片都会移到 recipe-item 中去

recipe-item V 层修改

这里主要就是接受之前在 recipe-list V 层的模板

<div class="pull-left">
  <h4 class="list-group-item-heading">{{ recipe.name }}</h4>
  <p class="list-group-item-text">{{ recipe.description }}</p>
</div>
<span class="pull-right">
  <img
    [src]="recipe.imagePath"
    [alt]="recipe.name"
    class="image-responsive"
    style="max-height: 50px"
  />
</span>
recipe-item VM 层修改

这里通过 @Input 去接受父组件传来的数据以完成动态渲染,代码如下:

export class RecipeItemComponent {
  @Input() recipe: Recipe;
}

这里看到样式没有任何的变化,不过 recipe 的渲染部分是由 recipe-item 完成的:

在这里插入图片描述

选择当前展示 recipe

这部分的实现其实和 navigation 的实现差不多,逻辑也基本一样。

看一下目前的项目结构:

❯ tree src/app/
src/app/
├── recipes
│   ├── recipe-detail
│   ├── recipe-list
│   │   ├── recipe-item

只不过这里处理 Output 的是 recipe-list,接受子组件传来的参数是 recipe,并且 recipe 需要将传过来的数据再传递到 recipe-detail 进行展示

⚠️:我这里偷了个懒,直接从 recipe-list 开始往上走。原本的写法应该是所有的东西都放在 recipe-item 里,然后一个个 emit event,因为 Angular 默认情况下是阻止 event propagation 的,所以只能手动一个个往上送(bubbling)

recipe-list V 层添加点击事件
<a
  href="#"
  class="list-group-item clearfix"
  *ngFor="let recipe of recipes"
  [ngClass]="{ active: recipe === selectedRecipe }"
  (click)="onSelectRecipe(recipe)"
>
  <app-recipe-item [recipe]="recipe"></app-recipe-item>
</a>

这里的 a 标签完全是可以丢到 app-recipe-item 去实现的,不过事件一旦传到了 app-recipe-item,就新创建了一个父子之间的沟通,所以需要从 app-recipe-item 再向上 propagate 一个事件,所以这里偷懒了

recipe-list VM 层添加选择事件

这里是对 onSelectRecipe 进行的处理,这里本身也是一个事件的中转站,它需要向 recipes 这一层去发送事件,使得当前被选中的对象可以传到 recipe-detail 中被展示:

export class RecipeListComponent {
  selectedRecipe: Recipe = this.recipes[0];
  @Output() activeRecipeChanged = new EventEmitter<Recipe>();

  ngOnInit() {
    this.activeRecipeChanged.emit(this.recipes[0]);
  }

  onSelectRecipe(recipe: Recipe) {
    this.selectedRecipe = recipe;
    this.activeRecipeChanged.emit(recipe);
  }
}

💡:我这里是用了 ngOnInit 在组件渲染时自动旋转数组中第一个对象,这个根据业务条件设置,也可以不选择这种做法。

recipe V 层添加绑定事件

这里除了新增事件绑定之外,还需要将绑定的对象传到 recipe-detail 中去,主要修改如下:

<div class="row">
  <div class="col-md-5">
    <app-recipe-list
      (activeRecipeChanged)="onRecipeChange($event)"
    ></app-recipe-list>
  </div>
  <div class="col-md-7">
    <app-recipe-detail
      [activeRecipe]="activeRecipe"
      *ngIf="activeRecipe; else noActiveRecipe"
    ></app-recipe-detail>
    <ng-template #noActiveRecipe>
      <p>Please create and select a recipe to view the detailed information</p>
    </ng-template>
  </div>
</div>

这里使用了 ngIf 进行条件控制,预防的是一个边界条件,也就是当数组为空时,recipes[0] 会是一个 undefined,这样会影响 recipe-detail 的后续操作

recipe VM 层添加接受修改数据

VM 层变动不大,主要是接受传递的事件,然后保存传上来的 recipe 即可:

export class RecipesComponent {
  activeRecipe: Recipe;

  onRecipeChange($event: Recipe) {
    this.activeRecipe = $event;
  }
}
recipe-detail

这里主要就是用 @Input 接受一下传来的数据,在 V 层渲染出来即可,变动比较少,代码就都贴下面了,V 层除了变量名基本没有变动:

export class RecipeDetailComponent {
  @Input() activeRecipe: Recipe;
}
<div class="row">
  <div class="col-xs-12">
    <img
      src="{{ activeRecipe.imagePath }}"
      alt=" {{ activeRecipe.name }} "
      class="img-responsive"
    />
  </div>
</div>
<div class="row">
  <div class="col-xs-12">
    <h1>{{ activeRecipe.name }}</h1>
  </div>
</div>
<div class="row">
  <div class="col-xs-12">
    <div class="btn-group">
      <button type="button" class="btn btn-primary dropdown-toggle">
        Manage Recipe <span class="caret"></span>
      </button>
      <ul class="dropdown-menu">
        <li><a href="#">To Shopping List</a></li>
        <li><a href="#">Edit Recipe</a></li>
        <li><a href="#">Delete Recipe</a></li>
      </ul>
    </div>
  </div>
</div>
<div class="row">
  <div class="col-xs-12">{{ activeRecipe.description }}</div>
</div>
<div class="row">
  <div class="col-xs-12">Ingredients</div>
</div>

最后效果如下:

在这里插入图片描述

⚠️:后来发现图片的路径没放进去,HTML 部分更新了,但是动图没更新

shopping-list

这里的应用主要是 local reference 和 @ViewChild,wireframe 中看到有两个输入:

在这里插入图片描述

所以这里两种方式都会用上

导入 FormsModule

不导入 FormsModule 所有的实现都会以默认的 HTML 实现,这里的 button 类型是 submit,也就是说会触发一个提交的 action,一般情况下会导致页面重新刷新

导入 FormsModule 会让 Angular 接收所有传来的参数,并且让 Angular 癌性决定后面的操作

shopping-list V 层修改

这里主要修改的部分就是在两个 input 中加入了 local reference,并且在 submit 的 button 那里绑定了点击时间,并且将 nameInput 作为 reference 传了过去

<div class="row">
  <div class="col-xs-12">
    <form>
      <div class="row">
        <div class="col-sm-5 form-group">
          <label for="name">Name</label>
          <input type="text" id="name" class="form-control" #nameInput />
        </div>
        <div class="col-sm-2 form-group">
          <label for="amount">Amount</label>
          <input type="number" id="amount" class="form-control" #amountInput />
        </div>
      </div>
      <div class="row">
        <div class="col-xs-12">
          <div class="btn-toolbar">
            <button
              class="btn btn-success mr-2"
              type="submit"
              (click)="onAddIngredient(nameInput)"
            >
              Add
            </button>
            <button class="btn btn-danger mr-2" type="button">Delete</button>
            <button class="btn btn-primary" type="button">Edit</button>
          </div>
        </div>
      </div>
    </form>
  </div>
</div>
shopping-list VM 层修改

这里比较简单,使用 @ViewChild 绑定当前 V 层的 local reference,并且接受点击事件传来的 #nameInput,最后按照老规矩,创建一个 EventEmitter 让父组件监听:

export class ShoppingEditComponent {
  @ViewChild('amountInput', { static: true })
  amountInput: ElementRef;
  @Output() ingredientAdded = new EventEmitter<Ingredient>();

  onAddIngredient(nameInput: HTMLInputElement) {
    this.ingredientAdded.emit({
      name: nameInput.value,
      amount: this.amountInput.nativeElement.value,
    });
  }
}
shopping-cart 接受子组件传来的参数

这里也比较简单,就放在一起写了

<div class="row">
  <div class="col-xs-10">
    <app-shopping-edit
      (ingredientAdded)="onAddIngredient($event)"
    ></app-shopping-edit>
    <hr />
    <ul class="list-group">
      <a
        class="list-group-item"
        style="cursor: pointer"
        *ngFor="let ingredient of ingredients"
      >
        {{ ingredient.name }} ({{ ingredient.amount }})
      </a>
    </ul>
  </div>
</div>
import { Component } from '@angular/core';
import { Ingredient } from '../shared/ingredient.model';

@Component({
  selector: 'app-shopping-list',
  templateUrl: './shopping-list.component.html',
  styleUrl: './shopping-list.component.css',
})
export class ShoppingListComponent {
  ingredients: Ingredient[] = [
    new Ingredient('Apples', 5),
    new Ingredient('Tomatoes', 10),
  ];

  onAddIngredient(ingredient: Ingredient) {
    this.ingredients.push(ingredient);
  }
}

最后完成效果如下:

在这里插入图片描述


至此一个功能大抵完成的动态页面就写完了

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值