预览组件
现在让我们来创建标签预览组件,预期效果如下:
用命令行来创建TagPreviewComponent组件的手脚架,它将是Create页面和Complete页面的子组件,所以把这个组件放在app文件夹下:
$ ng g component tag- preview
预览组件脚本
打开tag-preview.component.ts文件,修改如下:
import { Component, OnChanges, Input } from '@angular/core' ; import { PetTag } from './../core/pet-tag.model' ; @Component ( { selector: 'app-tag-preview' , templateUrl: './tag-preview.component.html' , styleUrls: [ './tag-preview.component.css' ] } ) export class TagPreviewComponent implements OnChanges { @Input ( ) petTag: PetTag; imgSrc = '' ; tagClipText: string; gemsText: string; constructor ( ) { } ngOnChanges ( ) { this . imgSrc = `/assets/images/ ${ this . petTag. shape} .svg` ; this . tagClipText = this . boolToText ( this . petTag. clip) ; this . gemsText = this . boolToText ( this . petTag. gems) ; } private boolToText ( bool: boolean) { return bool ? 'Yes' : 'No' ; } }
TagPreviewComponent是接受父组件CreateComponent输入的木偶子组件,它并不向父组件传递数据。引入Input装饰器以及OnChanges生命周期函数钩子,同时需要引入PetTag表明输入的类型。
TagPreviewComponent类需要实现OnChanges接口,也就是在该类中调用ngOnChanges方法。每当组件的输入参数发生变化时,ngOnChanges就会执行。这样才能实现在用户编辑修改的时候实时预览效果。
从父组件中接受的数据@Input() petTag是个状态对象,数据类型是之前定义的PetTag。比如,一个petTag对象可能是这样的:
{ shape: 'bone' , font: 'serif' , text: 'Fawkes' , clip: true , gems: false , complete: false }
我们希望友好直观地展示数据,所以我们将会显示标签的形状图片,文案,以及是否包含了clip和gems。
当用户进行操作时,需要专门设置一下图片的路径以及clip和gems的文案(Yes或者No)。输入是由CreateComponent对tagState$这个可观察对象的订阅提供。
预览组件模版
打开tag-preview.component.html文件,添加代码如下:
< ! -- src/ app/ tag- preview/ tag- preview. component. html -- > < div * ngIf= "petTag.shape" class = "row tagView-wrapper" > < div class = "col-sm-12" > < div class = "tagView {{petTag.shape}}" > < img [ src] = "imgSrc" / > < div class = "text {{petTag.font}}" > { { petTag. text} } < / div> < / div> < p class = "text-center" > < strong> Tag clip: < / strong> { { tagClipText} } < br> < strong> Gems: < / strong> { { gemsText} } < / p> < / div> < / div>
预览将会在用户选择标签形状之后显示。用一个shape样式类来控制显示正确的形状图片,同时也会显示标签的文字,字体是用font样式类来控制的。最后,直接标注出用户是否选择了包含clip,gems两个特性。
预览组件样式
之前我们定义4个标签形状:骨头形,方形,圆形,心形。为了优雅地展示预览效果,我们需要额外的样式。打开tag-preview.component.css样式,添加如下:
. tagView- wrapper { padding- top: 20px; } . tagView { height: 284px; position: relative; width: 100 % ; } img { display: block; height: 100 % ; margin: 0 auto; width: auto; } . text { font- size: 48px; position: absolute; text- align: center; text- shadow: 1px 1px 0 rgba ( 255 , 255 , 255 , . 8 ) ; top: 99px; width: 100 % ; } . bone . text, . rectangle . text { font- size: 74px; top: 85px; } . sans- serif { font- family: Arial, Helvetica, sans- serif; } . serif { font- family: Georgia, 'Times New Roman' , Times, serif; }
除了一些定位之外,我们根据不同形状设置了不同的字体大小,根据用户选择设置不同字体。
现在,我们可以添加到Create页面了。
将预览组件添加至Create页面
打开create.component.htm,将该组件添加至最底部:
< ! -- src/ app/ pages/ create/ create. component. html -- > . . . < app- tag- preview [ petTag] = "petTag" > < / app- tag- preview>
方括号[...]单向绑定属性,我们在CreateComponen组件的订阅tagStateSubscription中已经创建了petTag,现在将其传递给预览组件。
现在,我们应该可以看到标签的实时预览效果了:
提交完成标签
有了标签生成器和标签预览之后,添加一个Done按钮来提交创建好的标签到Complete页面。完成之后,页面如下:
我们已经在CreateComponent中创建了submit()方法,该方法派发带有载体的COMPLETE行为至归约器。我们只需要在create.component.html页面创建一个调用该方法的按钮即可:
< ! -- src/ app/ pages/ create/ create. component. html -- > . . . < div class = "row" > < div class = "col-sm-12 text-center" > < p class = "form-text text-muted" * ngIf= "petTag.shape" > Preview your customized tag above. < br> If you're happy with the results, < br> click the button below to finish! < / p> < p> < button class = "btn btn-success btn-lg" * ngIf= "petTag.shape" [ disabled] = "!done" ( click) = "submit()" routerLink= "/complete" > Done< / button> < / p> < / div> < / div>
我们之前在CreateComponent的tagStateSubscription对象中定义了done属性。 当done属性是false的时候,我们禁用提交按钮。
this . done = ! ! ( this . petTag. shape && this . petTag. text) ;
当标签有形状和文字时,我们就认为该标签已经可以提交了。如果用户已经添加了这些,他们就可以点击提交按钮完成标签的创建。同时,我们将用户导航至Complete页面。
“Complete”页面组件
在设置路由的时候,我们就搭建好了Complete页面的手脚架。当这个页面创建好之后,页面效果如下(前提是用户创建了一个标签):
“Complete”页面组件脚本
打开智能组件complete.component.ts,添加代码如下:
import { Component, OnInit, OnDestroy } from '@angular/core' ; import { Observable } from 'rxjs/Observable' ; import { Subscription } from 'rxjs/Subscription' ; import { Store } from '@ngrx/store' ; import { RESET } from './../../core/pet-tag.actions' ; import { PetTag } from './../../core/pet-tag.model' ; @Component ( { selector: 'app-complete' , templateUrl: './complete.component.html' } ) export class CompleteComponent implements OnInit , OnDestroy { tagState$: Observable< PetTag> ; private tagStateSubscription: Subscription; petTag: PetTag; constructor ( private store: Store< PetTag> ) { this . tagState$ = store. select ( 'petTag' ) ; } ngOnInit ( ) { this . tagStateSubscription = this . tagState$. subscribe ( ( state) = > { this . petTag = state; } ) ; } ngOnDestroy ( ) { this . tagStateSubscription. unsubscribe ( ) ; } newTag ( ) { this . store. dispatch ( { type: RESET } ) ; } }
CompleteComponent组件是可路由的容器组件。需要导入OnInit,
OnDestroy, Observable,
Subscription以及Store管理store订阅。同时,有个重置按钮,用户点击之后可以重新创建标签。store中的状态也会跟着重置,所以需要导入RESET行为,
PetTag数据类型以及默认初始值initialTag。
这个组件不需要Bootstrap以外的样式,所以我们可以把样式文件complete.component.css以及相应的引用删掉。
类似CreateComponent组件,我们需要创建tagState$的可观察对象tagStateSubscription,以及局部变量petTag。同时,我们需要创建PetTag数据类型的emptyTag变量,然后将其赋值为initialTag。
在构造函数中,将tagState$设为store可观察对象。然后在ngOnInit()函数中,订阅这个可观察对象,并且设置petTag属性。在ngOnDestroy()函数中,通过退订来销毁订阅。最后,newTag()函数派发RESET行为,使得应用的状态重置,用户可以继续定制他们的下一个标签了。
“Complete”页面组件模板
CompleteComponent组件的模板代码如下:
< ! -- src/ app/ pages/ complete/ complete. component. html -- > < div * ngIf= "petTag.complete" > < div class = "row" > < p class = "col-sm-12 alert alert-success" > < strong> Congratulations! < / strong> You've completed a pet ID tag for < strong> { { petTag. text} } < / strong> . Would you like to < a ( click) = "newTag()" routerLink= "/create" class = "alert-link" > create another? < / a> < / p> < / div> < app- tag- preview [ petTag] = "petTag" > < / app- tag- preview> < / div> < div * ngIf= "!petTag.complete" class = "row" > < p class = "col-sm-12 alert alert-danger" > < strong> Oops! < / strong> You haven't customized a tag yet. < a routerLink= "/create" class = "alert-link" > Click here to create one now. < / a> < / p> < / div>
首先展示恭喜用户成功为他们的宠物创建个性标签的提示,名字也会从petTag状态对象中取过来。同时,提供一个能重新创建新标签的链接,点击之后执行newTag()方法,该方法会将路由导航到创建页面重新开始。
接着,展示带有petTag的标签预览组件: 。
最后,如果用户在没有完成标签的定制下手动导航到/complete页面的话,我们就会显示一个错误信息。同样有个链接,可以让用户回到创建页面。错误页面效果如下:
至此,我们简单的Angular + ngrx/store应用完成了。
题外话:你可能不需要ngrx/store
状态管理库很棒,但请你确保在正式投入实际项目前你已经读过了这篇文章You Might Not Need Redux。
这个例子很简单,因为我们是用ngrx/store来教学。当你想用来投入实际项目时,你需要权衡一下必要性以及它的利弊。Angular(吸纳了RxJS)已经可以很方便地用service管理全局状态。因此,小型简单应用用局部变量就能很好地维护了。这种场景下,如果引入非必要的ngrx/store,可能会带来困扰和麻烦。
而在管理大型复杂项目的状态时,ngrx/store及其同类库是非常出色的工具。希望你现在已经有能力判断Redux和ngrx/store使用的原理。这样你就能知道如何以及何时应该使用状态管理库了。
附状态管理相关资源
附上一些学习状态管理的好资源:
ngrx/store on GitHub
@ngrx/store in 10 minutes
Comprehensive Introduction to @ngrx/store
ng-conf: Reactive Angular 2 with ngrx - Rob Womald
Angular 2 Service Layers: Redux, RxJS and Ngrx Store - When to Use a Store and Why?
Getting Started with Redux - Dan Abramov on Egghead.io
Angular的服务和组件通信在小型应用中的数据传递变得相当简单,但在复杂应用中仍是个棘手的问题。类似ngrx/store的全局store在组织状态管理中起到了很大的辅助作用。希望你现在已经准备好用ngrx/store构建自己的Angular 应用了!
关于本文
译者:@野草
译文:https://github.com/fezaoduke/TranslationInstitute/blob/master/手把手教你用ngrx管理Angular状态.md
作者:@Kim MaidaMr Raindrop
原文:https://auth0.com/blog/managing-state-in-angular-with-ngrx-store/