一. 模板内容
思路如下:
先让用户输入自己的手机号和密码,然后点击注册按钮跳转到输入验证码的界面。在验证码的界面输入正确的验证码之后,点击注册即可成功注册。
按照这个思路,“输入手机号和密码”视图以及“输入验证码”视图是两个完全不同的视图,需要用两个组件来实现。
但是,当用户输入完验证码以后,我们需要拿着用户之前输入的手机号和验证码发送到后台,看看这个验证码是否正确。
所以我们需要组件之间的数据通信,这个如果要靠非父子组件来实现稍微啰嗦了一点,又要往store里面加入新的内容。所以我们把注册页面和验证码页面当作父子组件,然后通过ng-template的配合*ngIf来判断什么时候显示注册页面,什么时候显示验证码页面。
<div class="register modal-content"><div class="modal-wrap" *ngIf="!showCode else code"><form nz-form nzLayout="vertical" [formGroup]="formModal" (ngSubmit)="onSubmit()"><nz-form-item><nz-form-label>手机号</nz-form-label><nz-form-control nzHasFeedback nzErrorTip="请填写正确的手机号"><nz-input-group nzPrefixIcon="mobile"><input type="tel" nz-input placeholder="请输入手机号" formControlName="phone"></nz-input-group></nz-form-control></nz-form-item><nz-form-item><nz-form-label>密码</nz-form-label><nz-form-control nzHasFeedback nzErrorTip="请输入密码"><nz-input-group nzPrefixIcon="lock"><input type="password" nz-input placeholder="请输入密码" formControlName="password"></nz-input-group></nz-form-control></nz-form-item><nz-form-item><nz-form-control><button type="submit" nz-button nzBlock>下一步</button></nz-form-control></nz-form-item></form></div><ng-template #code><div class="modal-wrap"><app-wy-check-code [timing]="timing"[codePass]="codePass"[phone]="formModal.get('phone').value"(onCheckCode)="onCheckCode($event)"(onRegister)="onCheckExist()"(onRepeatSendCode)="sendCode()"></app-wy-check-code></div></ng-template></div><div class="m-footer clearfix"><a (click)="changeType()">< 其它登陆方式</a></div>
二. 发送验证码
可以看到,当我们点击下一步的时候,就应该调用发送验证码的接口了。
除此之外我们还需要改变模板显示的内容,以及要给子组件(也就是验证码组件发送数据)。
总之,我们要做这三件事:
改变当前模板的显示状态,改为显示验证码的组件。由于我们的验证码需要进行60s的倒计时,所以这里注册一个异步事件,进行60s倒计时,然后传递给子组件。
注意我们调用验证码接口,后台是不会给我们返回验证码的。
onSubmit() {if (this.formModal.valid) {this.sendCode();}}sendCode() {this.memberService.sendCaptcha(this.formModal.get('phone').value).subscribe(() => {this.timing = 60;if (!this.showCode) {this.showCode = true;}interval(1000).pipe(take(60)).subscribe(() => {this.timing--;console.log(this.timing);}), error => this.messageService.error(error.message);});}
三. 验证码模板设计
验证码页面的设计说到底也是一个表单,不过中间显示四个验证码稍微复杂一点,我又把他抽出来做了一个组件。
<div class="check-code"><div class="js-mobwrap"><p class="s-fc3">你的手机号<strong class="s-fc1"><span class="js-mob">{{phone}}</span></strong></p><p class="s-fc4">为了安全,我们会给您发送短信验证码</p></div><div class="form"><form nz-form nzLayout="vertical" [formGroup]="formModel" (ngSubmit)="onSubmit()"><nz-form-item><nz-form-control><app-wy-code formControlName="code"></app-wy-code><div class="send clearfix"><span class="err" [hidden]="codePass">验证码错误</span><span class="txt" *ngIf="!showRepeatBtn else repeatBtn">{{timing}}s</span><ng-template #repeatBtn><span class="txt repeat" (click)="onRepeatSendCode.emit()">重新发送</span></ng-template></div></nz-form-control></nz-form-item><nz-form-item><nz-form-control><button nz-button nzType="primary" nzBlock>下一步</button></nz-form-control></nz-form-item></form></div>
app-wy-code组件的内容:
<div class="code-wrap clearfix" #codeWrap><div class="u-word" *ngFor="let item of inputArr; index as i"><input class="item" maxlength="1" /></div></div>
四. 验证码组件逻辑
这里提到的验证码组件逻辑是app-wy-code的逻辑,我们来想想这个4个input要完成什么样的功能。
首先,你每输入一个数字,就会触发下一个input的焦点事件。如果你手动选中一个input,那么该input就会触发焦点事件。不能输入除数字以外的内容,用户按下其他键我们就不相应。
接下来我们来完成这些内容,首先这个app-wy-code是一个angular的表单组件(这样才能和之前的form配合获取值),所以要实现ControlValueAccessor的接口。
export class WyCodeComponent implements OnInit, ControlValueAccessor {// ...setValue(code: string) {this.code = code;this.onChange(code);this.cdr.markForCheck();}writeValue(code: string): void {this.setValue(code);}registerOnChange(fn: any): void {this.onChange = fn;}registerOnTouched(fn: any): void {this.onTouched = fn;}}
另外,我们还要获取到这四个input的DOM,但是我们无法通过#+ViewChild来获得了。而且这些DOM都是通过*ngFor来实现的,所以我们要实现AfterViewInit接口。然后调用getElementsByClassname来获取这四个input的DOM。
ngAfterViewInit(): void {this.inputItems = this.elementRef.nativeElement.getElementsByClassName('item') as HTMLElement[];this.inputItems[0].focus();for (let i = 0; i < this.inputItems.length; ++i) {const item = this.inputItems[i];fromEvent(item, 'keyup').pipe(takeUntil(this.destroy$)).subscribe((event: KeyboardEvent) => this.listenup(event));fromEvent(item, 'click').pipe(takeUntil(this.destroy$)).subscribe(() => this.currentFocusIndex = i);}}
我们直接给这四个组件绑定了相应的keyup和click事件,具体实现如下:
listenup(event: KeyboardEvent) {const target = event.target as HTMLInputElement;const value = target.value;const isBackSpace = event.keyCode === BACKSPACE;if (/\D/.test(value)) {target.value = '';this.result[this.currentFocusIndex] = '';} else if (value) {this.result[this.currentFocusIndex] = value;this.currentFocusIndex = (this.currentFocusIndex + 1) % CODELEN;this.inputItems[this.currentFocusIndex].focus();} else if (isBackSpace) {this.result[this.currentFocusIndex] = '';this.currentFocusIndex = Math.max(this.currentFocusIndex - 1, 0);this.inputItems[this.currentFocusIndex].focus();}this.checkResult(this.result);}private checkResult(result: string[]) {const codeStr = result.join('');this.setValue(codeStr);}
这个代码已经写得非常清楚了,我就不再解释了。
注意当你监听一个事件的同时,也要想办法什么时候停止监听,这里使用destroy$ + OnDestroy接口来实现。
private destroy$ = new Subject();ngOnDestroy(): void {this.destroy$.next();this.destroy$.complete();}
destroy是怎么影响到我们的input的呢?原来是通过takeUtil操作符(见上面代码)
五. 验证验证码和判断用户是否已经注册
RT,我们还需要增加这两个功能。
什么时候发送验证码到服务端?答案是当你输完四个数字的时候。
可以在定义formGroup的时候限定四个数字,这样当你输入完的时候就会触发表单的statusChange事件。
constructor(private fb: FormBuilder) {this.formModel = this.fb.group({code: ['', [Validators.required, Validators.pattern(/\d{4}/)]]});const codeControl = this.formModel.get('code');codeControl.statusChanges.subscribe(status => {if (status === 'VALID') {this.onCheckCode.emit(codeControl.value);}});}
这里我直接把验证验证码交给父组件处理了,其实也就是调一个接口,不多BB。
对于判断用户是否存在,只要在onSubmit的时候处理下就行了。
onSubmit() {this.onRegister.emit();}
六. 验证码组件的其它功能
· 隐藏用户的手机信息
利用好typescript的get和set方法,以一个private的属性为中介,输出电话号码。
// Input和Set的组合@Input()set phone(phone: string) {const arr = phone.split('');arr.splice(3, 4, '****');this.phoneHideStr = arr.join('');}get phone() {return this.phoneHideStr;}
· 60s倒计时
这个我们在父组件已经实现过了,只要好好接收就行。
这里重点要讲的其实是60s到的时候我们显示“重新发送”,然后点击这个重
新发送就会真的重新发送。
// 每次输入的事件变化就会触发这个OnChanges事件// 每次改变我们就看看是不是要显示对应的按钮ngOnChanges(changes: SimpleChanges): void {if (changes.timing) {this.showRepeatBtn = this.timing <= 0;}}
显然这两个dom的事件都发生了变化,我选择使用*ngIf进行实现:
<span class="txt" *ngIf="!showRepeatBtn else repeatBtn">{{timing}}s</span><ng-template #repeatBtn><span class="txt repeat" (click)="onRepeatSendCode.emit()">重新发送</span></ng-template>