现在我们可以创建新的文章了,让我们创建一个文章页面,这样我们就可以把它们实际展示给大家看。文章页面需要有删除和收藏/取消收藏文章的功能。让我们修改文章服务来实现这个功能。

在ArticlesService中添加destroyfavoriteunfavorite方法

src/app/shared/services/articles.service.ts

  1. + destroy(slug) {
  2. + return this.apiService.delete('/articles/' + slug);
  3. + }
  4. + favorite(slug): Observable<Article> {
  5. + return this.apiService.post('/articles/' + slug + '/favorite');
  6. + }
  7. + unfavorite(slug): Observable<Article> {
  8. + return this.apiService.delete('/articles/' + slug + '/favorite');
  9. + }

很好!现在让我们创建文章页面。现在让我们创建文章页面。

创建ArticleComponent

src/app/article/article.component.html

  1. <div class="article-page">
  2. <div class="banner">
  3. <div class="container">
  4. <h1>{{ article.title }}</h1>
  5. </div>
  6. </div>
  7. <div class="container page">
  8. <div class="row article-content">
  9. <div class="col-md-12">
  10. <div [innerHTML]="article.body | markdown"></div>
  11. <ul class="tag-list">
  12. <li *ngFor="let tag of article.tagList"
  13. class="tag-default tag-pill tag-outline">
  14. {{ tag }}
  15. </li>
  16. </ul>
  17. </div>
  18. </div>
  19. <hr />
  20. <div class="article-actions">
  21. </div>
  22. <div class="row">
  23. <div class="col-xs-12 col-md-8 offset-md-2">
  24. <div *showAuthed="false">
  25. <a [routerLink]="['/login']">Sign in</a> or <a [routerLink]="['/register']">sign up</a> to add comments on this article.
  26. </div>
  27. </div>
  28. </div>
  29. </div>
  30. </div>

src/app/article/article.component.ts

  1. import { Component, OnInit } from '@angular/core';
  2. import { FormControl } from '@angular/forms';
  3. import { ActivatedRoute, Router } from '@angular/router';
  4. import {
  5. Article,
  6. ArticlesService,
  7. User,
  8. UserService
  9. } from '../shared';
  10. @Component({
  11. selector: 'article-page',
  12. templateUrl: './article.component.html'
  13. })
  14. export class ArticleComponent implements OnInit {
  15. article: Article;
  16. currentUser: User;
  17. canModify: boolean;
  18. isSubmitting = false;
  19. isDeleting = false;
  20. constructor(
  21. private route: ActivatedRoute,
  22. private articlesService: ArticlesService,
  23. private router: Router,
  24. private userService: UserService,
  25. ) { }
  26. ngOnInit() {
  27. // Retreive the prefetched article
  28. this.route.data.subscribe(
  29. (data: { article: Article }) => {
  30. this.article = data.article;
  31. }
  32. );
  33. // Load the current user's data
  34. this.userService.currentUser.subscribe(
  35. (userData: User) => {
  36. this.currentUser = userData;
  37. this.canModify = (this.currentUser.username === this.article.author.username);
  38. }
  39. );
  40. }
  41. onToggleFavorite(favorited: boolean) {
  42. this.article.favorited = favorited;
  43. if (favorited) {
  44. this.article.favoritesCount++;
  45. } else {
  46. this.article.favoritesCount--;
  47. }
  48. }
  49. onToggleFollowing(following: boolean) {
  50. this.article.author.following = following;
  51. }
  52. deleteArticle() {
  53. this.isDeleting = true;
  54. this.articlesService.destroy(this.article.slug)
  55. .subscribe(
  56. success => {
  57. this.router.navigateByUrl('/');
  58. }
  59. );
  60. }
  61. }

我们需要创建一个用于渲染标记的管道

如果你使用Angular的CLI创建了你的应用程序,你需要通过运行npm install marked --save并重新启动Webpack来安装marked的包。

创建MarkdownPipe

src/app/article/markdown.pipe.ts

  1. import { Pipe, PipeTransform } from '@angular/core';
  2. import * as marked from 'marked';
  3. @Pipe({name: 'markdown'})
  4. export class MarkdownPipe implements PipeTransform {
  5. transform(content: string): string {
  6. return marked(content, { sanitize: true });
  7. }
  8. }

我们还需要在组件初始化之前自动解析文章的数据。

创建ArticleResolver

src/app/article/article-resolver.service.ts

  1. import { Injectable, } from '@angular/core';
  2. import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
  3. import { Observable } from 'rxjs/Rx';
  4. import { Article, ArticlesService, UserService } from '../shared';
  5. @Injectable()
  6. export class ArticleResolver implements Resolve<Article> {
  7. constructor(
  8. private articlesService: ArticlesService,
  9. private router: Router,
  10. private userService: UserService
  11. ) {}
  12. resolve(
  13. route: ActivatedRouteSnapshot,
  14. state: RouterStateSnapshot
  15. ): Observable<any> {
  16. return this.articlesService.get(route.params['slug'])
  17. .catch((err) => this.router.navigateByUrl('/'));
  18. }
  19. }

最后,让我们创建文章模块,并将其导入到App模块中。

创建ArticleModule,并将其导入AppModule中

src/app/article/article.module.ts

  1. import { ModuleWithProviders, NgModule } from '@angular/core';
  2. import { RouterModule } from '@angular/router';
  3. import { ArticleComponent } from './article.component';
  4. import { ArticleResolver } from './article-resolver.service';
  5. import { MarkdownPipe } from './markdown.pipe';
  6. import { SharedModule } from '../shared';
  7. const articleRouting: ModuleWithProviders = RouterModule.forChild([
  8. {
  9. path: 'article/:slug',
  10. component: ArticleComponent,
  11. resolve: {
  12. article: ArticleResolver
  13. }
  14. }
  15. ]);
  16. @NgModule({
  17. imports: [
  18. articleRouting,
  19. SharedModule
  20. ],
  21. declarations: [
  22. ArticleComponent,
  23. MarkdownPipe
  24. ],
  25. providers: [
  26. ArticleResolver
  27. ]
  28. })
  29. export class ArticleModule {}

src/app/app.module.ts

  1. [...]
  2. import { RouterModule } from '@angular/router';
  3. import { AppComponent } from './app.component';
  4. +import { ArticleModule } from './article/article.module';
  5. import { AuthModule } from './auth/auth.module';
  6. import { EditorModule } from './editor/editor.module';
  7. import { HomeModule } from './home/home.module';
  8. [...]
  9. ],
  10. imports: [
  11. BrowserModule,
  12. + ArticleModule,
  13. AuthModule,
  14. EditorModule,
  15. HomeModule,
  16. [...]

我们现在可以查看文章了!但是,我们目前还不能看到文章的作者是谁、何时发布的、收藏/不收藏按钮、关注按钮、编辑/删除按钮等。然而,目前我们还不能看到文章的作者是谁,何时发布的,喜欢/不喜欢按钮,关注按钮,编辑/删除按钮等。

让我们创建一个组件,它可以在顶部和底部显示元数据,也可以显示其他元素,如关注/收藏按钮、删除/编辑、日期等。

创建ArticleMeta组件

src/app/shared/article-helpers/article-meta.component.html

  1. <div class="article-meta">
  2. <a [routerLink]="['/profile', article.author.username]">
  3. <img [src]="article.author.image" />
  4. </a>
  5. <div class="info">
  6. <a class="author"
  7. [routerLink]="['/profile', article.author.username]">
  8. {{ article.author.username }}
  9. </a>
  10. <span class="date">
  11. {{ article.createdAt | date: 'longDate' }}
  12. </span>
  13. </div>
  14. <ng-content></ng-content>
  15. </div>

src/app/shared/article-helpers/article-meta.component.ts

  1. import { Component, Input } from '@angular/core';
  2. import { Article } from '../models';
  3. @Component({
  4. selector: 'article-meta',
  5. templateUrl: './article-meta.component.html'
  6. })
  7. export class ArticleMetaComponent {
  8. @Input() article: Article;
  9. }

导出到我们的应用程序

src/app/shared/article-helpers/index.ts

  1. export * from './article-meta.component';

src/app/shared/index.ts

  1. +export * from './article-helpers';
  2. export * from './buttons';
  3. export * from './layout';
  4. export * from './list-errors.component';
  5. [...]

现在让我们创建收藏夹按钮,它将与我们创建关注按钮的方法超级相似。

创建FavoriteButton

src/app/shared/buttons/favorite-button.component.html

  1. <button class="btn btn-sm"
  2. [ngClass]="{ 'disabled' : isSubmitting,
  3. 'btn-outline-primary': !article.favorited,
  4. 'btn-primary': article.favorited }"
  5. (click)="toggleFavorite()">
  6. <i class="ion-heart"></i> <ng-content></ng-content>
  7. </button>

src/app/shared/buttons/favorite-button.component.ts

  1. import { Component, EventEmitter, Input, Output } from '@angular/core';
  2. import { Router } from '@angular/router';
  3. import { Article } from '../models';
  4. import { ArticlesService, UserService } from '../services';
  5. @Component({
  6. selector: 'favorite-button',
  7. templateUrl: './favorite-button.component.html'
  8. })
  9. export class FavoriteButtonComponent {
  10. constructor(
  11. private articlesService: ArticlesService,
  12. private router: Router,
  13. private userService: UserService
  14. ) {}
  15. @Input() article: Article;
  16. @Output() onToggle = new EventEmitter<boolean>();
  17. isSubmitting = false;
  18. toggleFavorite() {
  19. this.isSubmitting = true;
  20. this.userService.isAuthenticated.subscribe(
  21. (authenticated) => {
  22. // Not authenticated? Push to login screen
  23. if (!authenticated) {
  24. this.router.navigateByUrl('/login');
  25. return;
  26. }
  27. // Favorite the article if it isn't favorited yet
  28. if (!this.article.favorited) {
  29. this.articlesService.favorite(this.article.slug)
  30. .subscribe(
  31. data => {
  32. this.isSubmitting = false;
  33. this.onToggle.emit(true);
  34. },
  35. err => this.isSubmitting = false
  36. );
  37. // Otherwise, unfavorite the article
  38. } else {
  39. this.articlesService.unfavorite(this.article.slug)
  40. .subscribe(
  41. data => {
  42. this.isSubmitting = false;
  43. this.onToggle.emit(false);
  44. },
  45. err => this.isSubmitting = false
  46. );
  47. }
  48. }
  49. )
  50. }
  51. }

src/app/shared/buttons/index.ts

  1. +export * from './favorite-button.component';
  2. export * from './follow-button.component';

在SharedModule中声明并导出FavoriteButtonComponent和ArticleMetaComponent

src/app/shared/shared.module.ts

  1. [...]
  2. import { HttpModule } from '@angular/http';
  3. import { RouterModule } from '@angular/router';
  4. +import { ArticleMetaComponent } from './article-helpers';
  5. +import { FavoriteButtonComponent, FollowButtonComponent } from './buttons';
  6. import { ListErrorsComponent } from './list-errors.component';
  7. import { ShowAuthedDirective } from './show-authed.directive';
  8. [...]
  9. RouterModule
  10. ],
  11. declarations: [
  12. + ArticleMetaComponent,
  13. + FavoriteButtonComponent,
  14. FollowButtonComponent,
  15. ListErrorsComponent,
  16. ShowAuthedDirective
  17. ],
  18. exports: [
  19. + ArticleMetaComponent,
  20. CommonModule,
  21. + FavoriteButtonComponent,
  22. FollowButtonComponent,
  23. FormsModule,
  24. ReactiveFormsModule,
  25. [...]

现在让我们把它们添加到页面上

将FavoriteButtonComponent和ArticleMetaComponent添加到ArticleComponent中

src/app/article/article.component.html

  1. <div class="article-page">
  2. <div class="banner">
  3. <div class="container">
  4. <h1>{{ article.title }}</h1>
  5. +
  6. + <article-meta [article]="article">
  7. +
  8. + <span [hidden]="!canModify">
  9. + <a class="btn btn-sm btn-outline-secondary"
  10. + [routerLink]="['/editor', article.slug]">
  11. + <i class="ion-edit"></i> Edit Article
  12. + </a>
  13. +
  14. + <button class="btn btn-sm btn-outline-danger"
  15. + [ngClass]="{disabled: isDeleting}"
  16. + (click)="deleteArticle()">
  17. + <i class="ion-trash-a"></i> Delete Article
  18. + </button>
  19. + </span>
  20. +
  21. + <span [hidden]="canModify">
  22. + <follow-button
  23. + [profile]="article.author"
  24. + (onToggle)="onToggleFollowing($event)">
  25. + </follow-button>
  26. +
  27. + <favorite-button
  28. + [article]="article"
  29. + (onToggle)="onToggleFavorite($event)">
  30. + {{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article <span class="counter">({{ article.favoritesCount }})</span>
  31. + </favorite-button>
  32. + </span>
  33. +
  34. + </article-meta>
  35. </div>
  36. </div>
  37. <div class="container page">
  38. <div class="row article-content">
  39. <div class="col-md-12">
  40. <div [innerHTML]="article.body | markdown"></div>
  41. <ul class="tag-list">
  42. <li *ngFor="let tag of article.tagList"
  43. class="tag-default tag-pill tag-outline">
  44. {{ tag }}
  45. </li>
  46. </ul>
  47. </div>
  48. </div>
  49. <hr />
  50. <div class="article-actions">
  51. + <article-meta [article]="article">
  52. +
  53. + <span [hidden]="!canModify">
  54. + <a class="btn btn-sm btn-outline-secondary"
  55. + [routerLink]="['/editor', article.slug]">
  56. + <i class="ion-edit"></i> Edit Article
  57. + </a>
  58. +
  59. + <button class="btn btn-sm btn-outline-danger"
  60. + [ngClass]="{disabled: isDeleting}"
  61. + (click)="deleteArticle()">
  62. + <i class="ion-trash-a"></i> Delete Article
  63. + </button>
  64. + </span>
  65. +
  66. + <span [hidden]="canModify">
  67. + <follow-button
  68. + [profile]="article.author"
  69. + (onToggle)="onToggleFollowing($event)">
  70. + </follow-button>
  71. +
  72. + <favorite-button
  73. + [article]="article"
  74. + (onToggle)="onToggleFavorite($event)">
  75. + {{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article <span class="counter">({{ article.favoritesCount }})</span>
  76. + </favorite-button>
  77. + </span>
  78. +
  79. + </article-meta>
  80. </div>
  81. <div class="row">
  82. <div class="col-xs-12 col-md-8 offset-md-2">
  83. <div *showAuthed="false">
  84. <a [routerLink]="['/login']">Sign in</a> or <a [routerLink]="['/register']">sign up</a> to add comments on this article.
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </div>

最后,让我们更新编辑器,成功后导航到文章页面。

更新EditorComponent以导航到文章页面

src/app/editor/editor.component.ts

  1. [...]
  2. this.articlesService
  3. .save(this.article)
  4. .subscribe(
  5. + article => this.router.navigateByUrl('/article/' + article.slug),
  6. err => {
  7. this.errors = err;
  8. this.isSubmitting = false;
  9. [...]

你可以将你的代码与Github上的有效代码进行比较,或者在本地检查分支:

  1. git checkout m-1