用户应该能够创建和编辑和自己的文章。在这里我们要构建出这个功能,但在创建文章编辑器之前,我们首先要创建文章本身的模型。

下面的Article模型是基于Api规范的。

创建Article模型

src/app/shared/models/article.model.ts

  1. import { Profile } from './profile.model';
  2. export class Article {
  3. slug: string;
  4. title: string = '';
  5. description: string = '';
  6. body: string = '';
  7. tagList: Array<string> = [];
  8. createdAt: string;
  9. updatedAt: string;
  10. favorited: boolean;
  11. favoritesCount: number;
  12. author: Profile;
  13. }

src/app/shared/models/index.ts

  1. +export * from './article.model';
  2. export * from './errors.model';
  3. export * from './profile.model';
  4. export * from './user.model';

接下来,我们需要创建一个服务,负责根据API规范创建和检索文章。

创建ArticlesService

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

  1. import { Injectable } from '@angular/core';
  2. import { URLSearchParams } from '@angular/http';
  3. import { Observable } from 'rxjs/Rx';
  4. import 'rxjs/add/operator/map';
  5. import 'rxjs/add/operator/catch';
  6. import { ApiService } from './api.service';
  7. import { Article } from '../models';
  8. @Injectable()
  9. export class ArticlesService {
  10. constructor (
  11. private apiService: ApiService
  12. ) {}
  13. get(slug): Observable<Article> {
  14. return this.apiService.get('/articles/' + slug)
  15. .map(data => data.article);
  16. }
  17. save(article): Observable<Article> {
  18. // If we're updating an existing article
  19. if (article.slug) {
  20. return this.apiService.put('/articles/' + article.slug, {article: article})
  21. .map(data => data.article);
  22. // Otherwise, create a new article
  23. } else {
  24. return this.apiService.post('/articles/', {article: article})
  25. .map(data => data.article);
  26. }
  27. }
  28. }

src/app/shared/services/index.ts

  1. export * from './api.service';
  2. +export * from './articles.service';
  3. export * from './auth-guard.service';
  4. export * from './jwt.service';
  5. export * from './profiles.service';
  6. [...]

…我们需要将它包含在AppModule中,以便在我们的应用程序中使用它。

src/app/app.module.ts

  1. [...]
  2. import { AppComponent } from './app.component';
  3. import { AuthModule } from './auth/auth.module';
  4. import { HomeModule } from './home/home.module';
  5. import { ProfileModule } from './profile/profile.module';
  6. import { SettingsModule } from './settings/settings.module';
  7. import {
  8. ApiService,
  9. + ArticlesService,
  10. AuthGuard,
  11. FooterComponent,
  12. HeaderComponent,
  13. [...]
  14. ],
  15. providers: [
  16. ApiService,
  17. + ArticlesService,
  18. AuthGuard,
  19. JwtService,
  20. ProfilesService,
  21. [...]

厉害!现在,在编辑页面上,如果有文章的slug存在,我们要预取文章。我们将创建一个resolution来自动完成这个任务。

创建EditableArticleResolver

src/app/editor/editable-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 EditableArticleResolver 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. .map( article => {
  18. if (this.userService.getCurrentUser().username === article.author.username) {
  19. return article;
  20. } else {
  21. this.router.navigateByUrl('/');
  22. }
  23. }
  24. ).catch((err) => this.router.navigateByUrl('/'));
  25. }
  26. }

现在我们可以创建编辑器页面本身。

创建EditorComponent

src/app/editor/editor.component.html

  1. Create the EditorComponent
  2. src/app/editor/editor.component.html
  3. <div class="editor-page">
  4. <div class="container page">
  5. <div class="row">
  6. <div class="col-md-10 offset-md-1 col-xs-12">
  7. <list-errors [errors]="errors"></list-errors>
  8. <form [formGroup]="articleForm">
  9. <fieldset [disabled]="isSubmitting">
  10. <fieldset class="form-group">
  11. <input class="form-control form-control-lg"
  12. formControlName="title"
  13. type="text"
  14. placeholder="Article Title" />
  15. </fieldset>
  16. <fieldset class="form-group">
  17. <input class="form-control"
  18. formControlName="description"
  19. type="text"
  20. placeholder="What's this article about?" />
  21. </fieldset>
  22. <fieldset class="form-group">
  23. <textarea class="form-control"
  24. formControlName="body"
  25. rows="8"
  26. placeholder="Write your article (in markdown)">
  27. </textarea>
  28. </fieldset>
  29. <fieldset class="form-group">
  30. <input class="form-control"
  31. type="text"
  32. placeholder="Enter tags"
  33. [formControl]="tagField"
  34. (keyup.enter)="addTag()" />
  35. <div class="tag-list">
  36. <span *ngFor="let tag of article.tagList"
  37. class="tag-default tag-pill">
  38. <i class="ion-close-round" (click)="removeTag(tag)"></i>
  39. {{ tag }}
  40. </span>
  41. </div>
  42. </fieldset>
  43. <button class="btn btn-lg pull-xs-right btn-primary" type="button" (click)="submitForm()">
  44. Publish Article
  45. </button>
  46. </fieldset>
  47. </form>
  48. </div>
  49. </div>
  50. </div>
  51. </div>

src/app/editor/editor.component.ts

  1. import { Component, OnInit } from '@angular/core';
  2. import { FormBuilder, FormGroup, FormControl } from '@angular/forms';
  3. import { ActivatedRoute, Router } from '@angular/router';
  4. import { Article, ArticlesService } from '../shared';
  5. @Component({
  6. selector: 'editor-page',
  7. templateUrl: './editor.component.html'
  8. })
  9. export class EditorComponent implements OnInit {
  10. article: Article = new Article();
  11. articleForm: FormGroup;
  12. tagField = new FormControl();
  13. errors: Object = {};
  14. isSubmitting: boolean = false;
  15. constructor(
  16. private articlesService: ArticlesService,
  17. private route: ActivatedRoute,
  18. private router: Router,
  19. private fb: FormBuilder
  20. ) {
  21. // use the FormBuilder to create a form group
  22. this.articleForm = this.fb.group({
  23. title: '',
  24. description: '',
  25. body: '',
  26. });
  27. // Optional: subscribe to value changes on the form
  28. // this.articleForm.valueChanges.subscribe(value => this.updateArticle(value));
  29. }
  30. ngOnInit() {
  31. // If there's an article prefetched, load it
  32. this.route.data.subscribe(
  33. (data: {article: Article}) => {
  34. if (data.article) {
  35. this.article = data.article;
  36. this.articleForm.patchValue(data.article);
  37. }
  38. }
  39. );
  40. }
  41. addTag() {
  42. // retrieve tag control
  43. let tag = this.tagField.value;
  44. // only add tag if it does not exist yet
  45. if (this.article.tagList.indexOf(tag) < 0) {
  46. this.article.tagList.push(tag);
  47. }
  48. // clear the input
  49. this.tagField.reset('');
  50. }
  51. removeTag(tagName: string) {
  52. this.article.tagList = this.article.tagList.filter((tag) => tag !== tagName);
  53. }
  54. updateArticle(values: Object) {
  55. (<any>Object).assign(this.article, values);
  56. }
  57. submitForm() {
  58. this.isSubmitting = true;
  59. // update the model
  60. this.updateArticle(this.articleForm.value);
  61. // post the changes
  62. this.articlesService
  63. .save(this.article)
  64. .subscribe(
  65. article => this.router.navigateByUrl('/editor/' + article.slug),
  66. err => {
  67. this.errors = err;
  68. this.isSubmitting = false;
  69. }
  70. );
  71. }
  72. }

现在我们可以创建编辑器模块了。请注意,我们有两条路线:一条用于新建文章,另一条用于编辑文章(API规范)(并使用EditableArticleResolver自动获取)。

创建EditorModule

src/app/editor/editor.module.ts

  1. import { ModuleWithProviders, NgModule } from '@angular/core';
  2. import { RouterModule } from '@angular/router';
  3. import { EditorComponent } from './editor.component';
  4. import { EditableArticleResolver } from './editable-article-resolver.service';
  5. import { AuthGuard, SharedModule } from '../shared';
  6. const editorRouting: ModuleWithProviders = RouterModule.forChild([
  7. {
  8. path: 'editor',
  9. component: EditorComponent,
  10. canActivate: [AuthGuard]
  11. },
  12. {
  13. path: 'editor/:slug',
  14. component: EditorComponent,
  15. canActivate: [AuthGuard],
  16. resolve: {
  17. article: EditableArticleResolver
  18. }
  19. }
  20. ]);
  21. @NgModule({
  22. imports: [
  23. editorRouting,
  24. SharedModule
  25. ],
  26. declarations: [
  27. EditorComponent
  28. ],
  29. providers: [
  30. EditableArticleResolver
  31. ]
  32. })
  33. export class EditorModule {}

最后,将EditorModule包含在AppModule中。

将EditorModule包含在AppModule中

src/app/app.module.ts

  1. [...]
  2. import { AppComponent } from './app.component';
  3. import { AuthModule } from './auth/auth.module';
  4. +import { EditorModule } from './editor/editor.module';
  5. import { HomeModule } from './home/home.module';
  6. import { ProfileModule } from './profile/profile.module';
  7. import { SettingsModule } from './settings/settings.module';
  8. import {
  9. ApiService,
  10. ArticlesService,
  11. AuthGuard,
  12. FooterComponent,
  13. HeaderComponent,
  14. [...]
  15. imports: [
  16. BrowserModule,
  17. AuthModule,
  18. + EditorModule,
  19. HomeModule,
  20. ProfileModule,
  21. rootRouting,
  22. [...]

我们的编辑器现在能用了!为了更方便地访问它,让我们在标题中添加一个链接。

在HeaderComponent中添加一个链接到编辑器

src/app/shared/layout/header.component.html

  1. [...]
  2. </li>
  3. <li class="nav-item">
  4. + <a class="nav-link"
  5. + routerLink="/editor"
  6. + routerLinkActive="active">
  7. <i class="ion-compose"></i>&nbsp;New Article
  8. </a>
  9. </li>
  10. [...]

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

  1. git checkout m-2