向全栈迈进——Angular+Tornado开发树洞博客(十一)

在上一篇博客中,我们开发了评论系统的前端部分,介绍了angular中模板的概念。在这篇博客中,我们将继续开发评论系统的前端部分,并介绍组件间通信的相关内容。
打开comments.component.ts文件,输入以下内容:

//comments.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { CommentService } from 'src/app/service/comment.service';

@Component({
  selector: 'app-comments',
  templateUrl: './comments.component.html',
  styleUrls: ['./comments.component.scss']
})
export class CommentsComponent implements OnInit {
  showComment:boolean[]
  @Input() commentTree:any;
  @Input() storyId:number;

  constructor() {
    this.showComment = [false];
    this.storyId = 0;
   }

  ngOnInit(): void {
  }

  showCommentForm(storyId:number){
    this.showComment[storyId] = !this.showComment[storyId];
  }
}

这个组件中有两个属性:commentTree和storyId,前者用于存放评论树内容,而后者表示当前评论树属于哪个故事。
由于我们这个控件要作为storyList的子控件使用,即需要由storyList指定comments组件要放在哪个故事下面,因此我们使用@Input()修饰器来修饰commentTree和storyId参数,表明storyList组件可以指定comments组件中这两个属性的值:
向全栈迈进——Angular+Tornado开发树洞博客(十一)
底下的showCommentForm函数用来反转showComment数组中对应元素的值,控制评论树中评论表单的隐现。
这样,我们这个comments的前端部分就开发完了,下面让我们实现另一个组件commentForm,
这个组件用于让用户发表评论。
打开commentform/commentform.component.html,输入以下代码:

<!--commentform.component.html-->
<form nz-form  [formGroup]="commentForm" (ngSubmit)="publishComment()">
    <nz-form-item>
      <nz-form-control>
          <input formControlName="content" nz-input placeholder="说点什么" />
      </nz-form-control>
    </nz-form-item>
    <nz-form-item>
      <nz-form-control>
        <button nz-button nzType="primary">发表</button>
      </nz-form-control>
    </nz-form-item>
</form>

这个组件没什么说的,就是一个简单的表单。
下面打开commentform.component.ts文件,输入以下代码:

//commentform.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd/message';
import { CookieService } from 'ngx-cookie-service';
import { CommentService } from 'src/app/service/comment.service';
import { commentData } from '../storylist/commentData';
import { refreshCommentInfo } from './refreshcommentinfo';

@Component({
  selector: 'app-commentform',
  templateUrl: './commentform.component.html',
  styleUrls: ['./commentform.component.scss']
})
export class CommentformComponent implements OnInit {
  newComment:commentData;
  author:string;
  @Input() commentBody:string;
  @Input() storyId:number;
  @Output() refreshCommentEvent: EventEmitter<refreshCommentInfo>;


  commentForm = new FormGroup({
    content:new FormControl('')
  });

  constructor(private cookie:CookieService,private commentService:CommentService,private message:NzMessageService,private router:Router) {
    this.newComment = {
      author:'',
      content:'',
      commentBody:''
    }
    this.author = this.cookie.get('currentuser');
    this.commentBody = ''
    this.storyId = 0
    this.refreshCommentEvent = new EventEmitter();
   }

  ngOnInit(): void {
    console.log('comment form ' + this.commentBody)
    console.log('storyid:' + this.storyId)
  }

  publishComment(){
    this.newComment = this.commentForm.value;
    this.newComment.author = this.author;
    this.newComment.commentBody = this.commentBody;
    this.commentService.writeComment(this.newComment).subscribe((data:any)=>{
      if (data){
        if (data['result'] == 'Success'){
          console.log('Success comment');
          console.log('storyid:' + this.storyId)
          console.log('external storyId:' + this.newComment.commentBody)
          this.message.create('success','评论成功')
          this.commentForm.setValue({'content':''});
          setTimeout(() => {
            this.refreshCommentEvent.emit({'storyId':this.storyId,'externalStoryId':this.newComment.commentBody});
          }, 800);
        }
      }
    })
  }

}

我们来看一下这个组件中用到了哪些变量:

  • newComment:类型为之前定义的commentData,用于接收用户输入的新评论;
  • author:评论作者;
  • commentBody:表示这条评论的主体是谁,可以是故事也可以是别的评论;
  • storyId:评论所属的故事主体;
  • refreshCommentEvent:当评论完成时触发的事件,这里暂时不讲;
  • commentForm:FormGroup,用于处理表单。
    因为我们的这个表单控件也是作为storyList和comments组件的子组件使用的,我们同样使用@Input()来修饰commentBody和storyId变量;然而,对于refreshCommentEvent我们使用了@Output()修饰器来装饰它,表示它是一个向父组件传递信息的变量:
    向全栈迈进——Angular+Tornado开发树洞博客(十一)
    这里用@Output()的目的是,当用户在评论树中发表了评论后,通知storyList重新抓取当前故事中的评论,实现立即显示新评论的功能。
    这个commentForm只有一个函数:publishComment,用于让用户发布新评论,并在成功发布评论后向父组件发送消息,让父组件重新获取当前故事的评论树。
    接下来,让我们修改storylist.component.html,调用我们刚刚写好的commentform和comments组件:
<!--storylist.component.html-->
<div *ngFor="let story of storyList" >
    <nz-card nzTitle="{{story.title}}" >
	<!--...-->
    <span *nzSpaceItem ><i nz-icon nzType="comment" nzTheme="outline"></i><a (click)="showAllComment(story.id,story.externalId)"> 评论</a> </span>
	<!--...-->
    </nz-space>
    </nz-card>
    <div *ngIf="showComment[story.id]">
    <nz-card nzTitle="评论" >
        <app-commentform [commentBody]="story.externalId" [storyId]="story.id" (refreshCommentEvent)="refreshComment($event)"></app-commentform>
        <app-comments [commentTree] = "commentTree" [storyId]="story.id"></app-comments>
    </nz-card>
    </div>
    <br>
</div>

我们为之前的评论标签添加一个a标签,让它调用我们稍后要实现的showAllComment函数来控制评论栏的隐现。这里我们用一个存有storyId的布尔型数组showComment来控制评论栏的隐现,如果对应storyId的值是true,则显示该故事的评论栏以及底下的评论树。
在commentform控件中,我们用**[commentBody][storyId]的语法向commentForm中的同名属性传值,表明评论框的评论是给这个故事的。类似的,我们在comments控件中也会传入[commentTree][storyId]**的属性,以便显示故事的评论树。
我们打开storylist.component.ts文件,向其中添加以下代码:

//storylist.component.ts
import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { StoryService } from 'src/app/service/story.service';
import { storyDetail } from './storeDetail';
import { FormControl,FormGroup } from '@angular/forms';
import { CommentService } from 'src/app/service/comment.service';
import { refreshCommentInfo } from '../commentform/refreshcommentinfo';

@Component({
  selector: 'app-storylist',
  templateUrl: './storylist.component.html',
  styleUrls: ['./storylist.component.scss']
})
export class StorylistComponent implements OnInit {
  storyList:storyDetail[];
  showComment:boolean[];
  public commentTree:any;
  commentForm = new FormGroup({
    content:new FormControl('')
  });

  constructor(private storyService:StoryService,private commentService:CommentService) {
  	//...
    this.showComment = [false];
   }
  ngOnInit(): void {

  }
  //...
  showAllComment(storyId:number,externalStoryId:string){
    this.showComment[storyId] = !this.showComment[storyId];
    this.getAllComment(storyId,externalStoryId);
  }
  getAllComment(storyId:number,externalStoryId:string){
    const commentowner = {'storyId':externalStoryId}
    this.commentService.getComment(commentowner).subscribe((data:any) =>{
      console.log(data['result']);
      //this.data = data;
      this.commentTree[storyId] = data['result'];
    })
  }
  refreshComment(info:refreshCommentInfo){
    this.showComment[info['storyId']] = true;
    this.getAllComment(info['storyId'],info['externalStoryId']);
  }
}

这里我们定义了两个新变量:showComment和commentTree,前者用于控制评论栏的隐现,而后者用于显示故事的评论树。在构造函数中,我们将showComment设为false,即在默认情况下不显示评论。
然后,我们实现一个showAllComment函数,这个函数的功能之一是反转showComment数组的指定元素的值,从而实现点击评论按钮来隐藏显示评论栏;功能之二是稍后要实现的获取该故事的评论树getAllComment函数;而refreshComment函数用于用户发表评论后重新获取当前故事的评论。
下面再让我们实现评论的service部分,打开cmd窗口,输入以下命令:

ng g s service/comment

然后打开comment.service.ts文件,输入以下代码:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { commentOwner } from '../story/comments/commentowner';
import { commentData } from '../story/storylist/commentData';

@Injectable({
  providedIn: 'root'
})
export class CommentService {

  constructor(private http:HttpClient) { }

  writeComment(comment:commentData):Observable<commentData>{
    return this.http.post<commentData>('http://localhost:8000/writecomment',comment)
  }

  getComment(storyId:commentOwner):Observable<commentOwner>{
    return this.http.post<commentOwner>('http://localhost:8000/getcomment',storyId)
  }

}

这里我们实现两个service:writeComment和getComment。顾名思义,前者用于发表评论,后者用于获取评论。
这样,我们就完成了评论系统所有的前端开发部分。我们来总结一下在这两篇博客中用到的知识点:

  • <ng-template>:angular模板,可以通过#语法来为模板指定模板名,以便之后的复用。
  • ngTemplateOutlet:用在<ng-template>标签内部,表示当前的模板要复用哪个模板,值为上一条中通过#定义的模板名
  • ngTemplateOutletContext:决定复用的模板中的数据,其值为一个key-value对象。
  • ngFor let-xxx [ngForOf]:模板中使用的*ngFor语法形式。
  • @Input()修饰器:表明这个属性可以被父组件访问、赋值,在html代码中通过[属性名]为该属性赋值。
  • @Output()修饰器:表明这个属性可以传递数据给父组件,与@Input()作用相反。
    以上就是我们开发评论系统的前端部分目前用到的所有知识点,可以说模板的相关概念理解起来还是有点困难的,思想可能会有一点绕。
    在下一篇博客中,我们将继续实现评论系统的后端部分,希望大家继续关注~
上一篇:angular 父组件调用子组件的方法


下一篇:k8s 持久卷pv和pvc使用