使用 REST API 和 ASP.NET Core 2.2 构建 Angular 8 应用 - 第 2 部分
在本教程的第 1 部分中,我们构建了 ASP.NET Core 后端。
第 2 部分 - 创建 Angular 8 应用
现在,让我们最终开始 Angular 应用。我们将使用Node.js和Angular CLI生成 Angular 项目和必要的文件。
记住。Github仓库在这里:使用 .NET Core 2.2 和 Entity Framework 后端的 Angular 8 博客应用教程
先决条件
安装 Node.js 后,您可以打开 Node.js 命令提示符。
在命令提示符中执行此命令来安装 Angular 8 CLI:
npm install -g @angular/cli
这将全局安装最新的 Angular CLI,需要一点时间。完成后,您可以使用以下命令检查 Angular 版本:
ng --version
Node.js 命令提示符应如下所示:
现在,让我们转到 Visual Studio 后端所在的文件夹。使用 cd 命令执行此操作:
cd c:/projects/blog
我们将简单地将我们的 Angular 8 应用程序命名为 ClientApp。让我们执行创建 Angular 应用程序的命令:
ng new ClientApp
系统会提示我们一些问题。我们想使用路由(按 Y)和样式表格式:SCSS。然后让 Node 自行创建 Web 应用。这大约需要一分钟。
创建应用程序后,使用 cd 命令进入应用程序文件夹:
cd ClientApp
然后使用以下ng serve
命令构建并运行应用程序:
ng serve
命令提示符将如下所示:
构建成功,现在你可以使用 URL http://localhost:4200浏览到你的 Angular 应用。基本的 Angular 8 应用基于模板,它看起来像这样:
如果你看一下源代码,它将看起来像这样:
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>ClientApp</title> | |
<base href="/"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="icon" type="image/x-icon" href="favicon.ico"> | |
</head> | |
<body> | |
<app-root></app-root> | |
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="styles.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script></body> | |
</html> |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>ClientApp</title> | |
<base href="/"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="icon" type="image/x-icon" href="favicon.ico"> | |
</head> | |
<body> | |
<app-root></app-root> | |
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="styles.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script></body> | |
</html> |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>ClientApp</title> | |
<base href="/"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="icon" type="image/x-icon" href="favicon.ico"> | |
</head> | |
<body> | |
<app-root></app-root> | |
<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="styles.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script></body> | |
</html> |
这里有趣的是<app-root></app-root>
,它是 Angular 特有的,它告诉我们 Angular 应用程序将在哪里执行。
最后一个命令值得了解——它是Ctrl+C
关闭 Angular 应用程序,你应该按两次该命令来终止批处理作业并停止 ng serve。
Angular 开发的一大优点是,你保存在前端文件中的更改将立即反映在浏览器中。要实现这一点,应用程序需要处于运行状态。
但是对于某些更改,例如添加 Bootstrap,您需要重新启动应用程序才能使其正常工作。
Angular 8 基础知识
让我们停下来,退一步,学习一些 Angular 8 的基础知识。
Angular 8 是一个开源的客户端 JavaScript 框架,基于 TypeScript 并编译为 JavaScript。
Angular 8 架构包括以下内容:
-
模块
-
成分
-
模板、指令、数据绑定
-
服务和依赖注入
-
路由
您可以在官方文档中深入了解Angular 架构。以下是简要概述:
模块
Angular NgModule 是任何 Angular 应用的基础。每个 Angular 应用都有一个名为 AppModule 的根模块,它负责引导和启动应用。模块可以调用组件和服务。默认模块是app.module.ts。
成分
组件为我们提供了一个类和一个视图,它们是应用程序的一部分。类基于 TypeScript,视图则基于 HTML。所有 Angular 应用至少包含一个名为app.component.ts的组件。
模板、指令、数据绑定
模板将 HTML 与 Angular 标记结合在一起。指令提供逻辑,绑定标记将应用程序数据与 DOM 连接起来。
服务和依赖注入
服务类提供的应用逻辑不依赖于特定视图,而是在整个应用范围内共享。它们可以使用 @Injectable() 装饰器进行注入。组件类使用依赖注入保持整洁。
路由
Router NgModule 提供了定义应用导航的服务。其工作方式与浏览器的导航相同。
Visual Studio 2019 用于后端,VS Code 用于前端
虽然 Visual Studio 2019 在后端和前端方面都表现出色,但VS Code实际上更适合使用 Angular 等框架进行繁重的前端工作。我建议您尝试 VS Code,本教程中关于 Angular 应用程序的大部分说明也适用于 VS Code。
为了更轻松地在 VS Code 中进行前端和 Angular 开发,请安装这些扩展。您可以通过 VS Code 扩展模块轻松完成此操作。
当然,还有很多很棒的扩展,比如 Beautify 和 Path Intellisense,可以提高你的开发效率。一切都取决于你的喜好和风格。
在 VS Code 中,确保打开磁盘上的 ClientApp 文件夹并从那里开始工作。
向我们的 Angular 应用添加组件和服务
让我们继续构建 Angular 应用。首先,如果您尚未关闭与应用的连接,请在 Node.js 命令提示符中按两次 Ctrl+C。
接下来,让我们将 Bootstrap 4 添加到应用程序中。在 Node.js 命令提示符中执行以下命令:
npm install bootstrap --save
然后找到angular.json文件并编辑构建节点,使样式如下所示:
"styles": [ | |
"src/styles.scss", | |
"node_modules/bootstrap/dist/css/bootstrap.min.css" | |
] |
"styles": [ | |
"src/styles.scss", | |
"node_modules/bootstrap/dist/css/bootstrap.min.css" | |
] |
"styles": [ | |
"src/styles.scss", | |
"node_modules/bootstrap/dist/css/bootstrap.min.css" | |
] |
angular.json 构建节点应如下所示:
接下来,让我们创建组件。我们的博客应用程序将包含三个组件:
-
BlogPosts — 显示所有博客文章。
-
BlogPost — 显示特定的博客文章。
-
BlogPostAddEdit — 添加新的或编辑现有的博客文章。
要创建这些组件,请在 Node.js 命令提示符中执行以下命令:
ng generate component BlogPosts
ng generate component BlogPost
ng generate component BlogPost-AddEdit
在 ClientApp/src/app 下,组件现在位于:
如您所见,每个组件都有一个 .html 文件、scss 文件、spec.ts 文件和 component.ts 文件。
-
HTML 和 SCSS 用于视图。
-
spec.ts 用于测试。
-
component.ts 包含我们的组件类和逻辑。
当我们这样做时,让我们使用命令提示符来创建我们的服务:
ng generate service BlogPost
在 app 下创建一个新文件夹,并将其命名为services。将生成的两个服务文件移动到该文件夹中:
现在让我们离开组件和服务,看看app.module.ts文件。在这里我们导入模块和组件,声明它们,并添加提供程序。
我们从创建的应用中免费获得了一些功能。添加了必要的导入和一些模块。当我们在 Node.js 命令提示符中添加组件时,app.modules.ts 文件也会更新。然而,我们并没有获得所有方面的帮助。对于我们的博客应用,我们需要手动导入并添加一些模块。我们还需要导入我们的服务并将其添加到提供程序中。
让我们更新文件使其看起来像这样:
import { BrowserModule } from '@angular/platform-browser'; | |
import { NgModule } from '@angular/core'; | |
import { HttpClientModule } from '@angular/common/http'; | |
import { ReactiveFormsModule } from '@angular/forms'; | |
import { AppRoutingModule } from './app-routing.module'; | |
import { AppComponent } from './app.component'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
import { BlogPostService } from './services/blog-post.service'; | |
@NgModule({ | |
declarations: [ | |
AppComponent, | |
BlogPostsComponent, | |
BlogPostComponent, | |
BlogPostAddEditComponent | |
], | |
imports: [ | |
BrowserModule, | |
HttpClientModule, | |
AppRoutingModule, | |
ReactiveFormsModule | |
], | |
providers: [ | |
BlogPostService | |
], | |
bootstrap: [AppComponent] | |
}) | |
export class AppModule { } |
import { BrowserModule } from '@angular/platform-browser'; | |
import { NgModule } from '@angular/core'; | |
import { HttpClientModule } from '@angular/common/http'; | |
import { ReactiveFormsModule } from '@angular/forms'; | |
import { AppRoutingModule } from './app-routing.module'; | |
import { AppComponent } from './app.component'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
import { BlogPostService } from './services/blog-post.service'; | |
@NgModule({ | |
declarations: [ | |
AppComponent, | |
BlogPostsComponent, | |
BlogPostComponent, | |
BlogPostAddEditComponent | |
], | |
imports: [ | |
BrowserModule, | |
HttpClientModule, | |
AppRoutingModule, | |
ReactiveFormsModule | |
], | |
providers: [ | |
BlogPostService | |
], | |
bootstrap: [AppComponent] | |
}) | |
export class AppModule { } |
import { BrowserModule } from '@angular/platform-browser'; | |
import { NgModule } from '@angular/core'; | |
import { HttpClientModule } from '@angular/common/http'; | |
import { ReactiveFormsModule } from '@angular/forms'; | |
import { AppRoutingModule } from './app-routing.module'; | |
import { AppComponent } from './app.component'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
import { BlogPostService } from './services/blog-post.service'; | |
@NgModule({ | |
declarations: [ | |
AppComponent, | |
BlogPostsComponent, | |
BlogPostComponent, | |
BlogPostAddEditComponent | |
], | |
imports: [ | |
BrowserModule, | |
HttpClientModule, | |
AppRoutingModule, | |
ReactiveFormsModule | |
], | |
providers: [ | |
BlogPostService | |
], | |
bootstrap: [AppComponent] | |
}) | |
export class AppModule { } |
必要的模块如HttpClientModule
和ReactiveFormsModule
被导入。AppRoutingModule
和AppComponent
从一开始就已经为我们创建好了。
只需确保声明组件、将模块添加到导入并将我们的服务添加到提供商。
关于进出口只有一件事。
TypeScript 使用 EcmaScript 2015 中的模块概念。模块在其自身作用域内执行,而不是在全局作用域内执行。为了使一个模块的类、变量、函数等对其他模块可见,可以使用 export 。此外,为了使用其他模块中的某些内容,需要使用 import 。
设置路由
打开app-routing.module.ts。这里设置了路由,但尚未配置任何路由:
const routes: Routes = [];
更新文件使其看起来像这样:
import { NgModule } from '@angular/core'; | |
import { Routes, RouterModule } from '@angular/router'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
const routes: Routes = [ | |
{ path: '', component: BlogPostsComponent, pathMatch: 'full' }, | |
{ path: 'blogpost/:id', component: BlogPostComponent }, | |
{ path: 'add', component: BlogPostAddEditComponent }, | |
{ path: 'blogpost/edit/:id', component: BlogPostAddEditComponent }, | |
{ path: '**', redirectTo: '/' } | |
]; | |
@NgModule({ | |
imports: [RouterModule.forRoot(routes)], | |
exports: [RouterModule] | |
}) | |
export class AppRoutingModule { } |
import { NgModule } from '@angular/core'; | |
import { Routes, RouterModule } from '@angular/router'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
const routes: Routes = [ | |
{ path: '', component: BlogPostsComponent, pathMatch: 'full' }, | |
{ path: 'blogpost/:id', component: BlogPostComponent }, | |
{ path: 'add', component: BlogPostAddEditComponent }, | |
{ path: 'blogpost/edit/:id', component: BlogPostAddEditComponent }, | |
{ path: '**', redirectTo: '/' } | |
]; | |
@NgModule({ | |
imports: [RouterModule.forRoot(routes)], | |
exports: [RouterModule] | |
}) | |
export class AppRoutingModule { } |
import { NgModule } from '@angular/core'; | |
import { Routes, RouterModule } from '@angular/router'; | |
import { BlogPostsComponent } from './blog-posts/blog-posts.component'; | |
import { BlogPostComponent } from './blog-post/blog-post.component'; | |
import { BlogPostAddEditComponent } from './blog-post-add-edit/blog-post-add-edit.component'; | |
const routes: Routes = [ | |
{ path: '', component: BlogPostsComponent, pathMatch: 'full' }, | |
{ path: 'blogpost/:id', component: BlogPostComponent }, | |
{ path: 'add', component: BlogPostAddEditComponent }, | |
{ path: 'blogpost/edit/:id', component: BlogPostAddEditComponent }, | |
{ path: '**', redirectTo: '/' } | |
]; | |
@NgModule({ | |
imports: [RouterModule.forRoot(routes)], | |
exports: [RouterModule] | |
}) | |
export class AppRoutingModule { } |
我们导入必要的组件并使用路径更新路由,并告知将在这些路径中加载哪些组件。
{ path: '', component: BlogPostsComponent, pathMatch: 'full' }
这告诉我们我们将在应用程序开始页面上加载 BlogPostsComponent。
{ path: '**', redirectTo: '/' }
这告诉我们应用程序的所有无效路径都将被重定向到起始页。
打开app.component.html并更新文件使其如下所示:
<div class="container"> | |
<a [routerLink]="['/']" class="btn btn-info">Start</a> | |
<router-outlet></router-outlet> | |
</div> |
<div class="container"> | |
<a [routerLink]="['/']" class="btn btn-info">Start</a> | |
<router-outlet></router-outlet> | |
</div> |
<div class="container"> | |
<a [routerLink]="['/']" class="btn btn-info">Start</a> | |
<router-outlet></router-outlet> | |
</div> |
该<router-outlet></router-outlet>
元素将被正确的组件替换,并且该文件将用于应用程序中的所有组件。
现在,让我们使用ng serve
Node.js 命令提示符中的命令再次构建并运行该应用程序。Node 编译完成后,访问http://localhost:4200。起始页现在将如下所示:
这是 BlogPostsComponent 的实际效果。尝试访问http://localhost:4200/add,你会看到 BlogPostAddEditComponent 的视图。
如果您尝试浏览不存在的路径,您将被再次重定向到起始页。
构建和运行应用程序的不同方法
我们可以使用两种不同的方法来构建和运行我们的 Angular 应用程序:
-
Node.js 命令提示符和
ng serve
。 -
Visual Studio F5 命令和 IIS Express。
了解这一点很有用。最简单的方法就是使用 Visual Studio 构建并运行我们的 Angular 应用以及后端。为了使 Angular 应用正常运行,我们需要编辑Startup.cs文件以允许 SPA 静态文件。
在 Startup.cs 中,我们已经注释掉了 SPA 的配置。在ConfigureServices 方法中,取消注释以下services.AddSpaStaticFiles
部分:
services.AddSpaStaticFiles(configuration => | |
{ | |
configuration.RootPath = "ClientApp/dist"; | |
}); |
services.AddSpaStaticFiles(configuration => | |
{ | |
configuration.RootPath = "ClientApp/dist"; | |
}); |
services.AddSpaStaticFiles(configuration => | |
{ | |
configuration.RootPath = "ClientApp/dist"; | |
}); |
在Configure方法中,取消注释以下app.UseSpaStaticFiles()
行和app.UseSpa()
部分。之前我们已经有了app.UseMvc()
:
app.UseSpaStaticFiles(); | |
app.UseMvc(routes => | |
{ | |
routes.MapRoute( | |
name: "default", | |
template: "{controller}/{action=Index}/{id?}"); | |
}); | |
app.UseSpa(spa => | |
{ | |
// To learn more about options for serving an Angular SPA from ASP.NET Core, | |
// see https://go.microsoft.com/fwlink/?linkid=864501 | |
spa.Options.SourcePath = "ClientApp"; | |
if (env.IsDevelopment()) | |
{ | |
spa.UseAngularCliServer(npmScript: "start"); | |
} | |
}); |
app.UseSpaStaticFiles(); | |
app.UseMvc(routes => | |
{ | |
routes.MapRoute( | |
name: "default", | |
template: "{controller}/{action=Index}/{id?}"); | |
}); | |
app.UseSpa(spa => | |
{ | |
// To learn more about options for serving an Angular SPA from ASP.NET Core, | |
// see https://go.microsoft.com/fwlink/?linkid=864501 | |
spa.Options.SourcePath = "ClientApp"; | |
if (env.IsDevelopment()) | |
{ | |
spa.UseAngularCliServer(npmScript: "start"); | |
} | |
}); |
app.UseSpaStaticFiles(); | |
app.UseMvc(routes => | |
{ | |
routes.MapRoute( | |
name: "default", | |
template: "{controller}/{action=Index}/{id?}"); | |
}); | |
app.UseSpa(spa => | |
{ | |
// To learn more about options for serving an Angular SPA from ASP.NET Core, | |
// see https://go.microsoft.com/fwlink/?linkid=864501 | |
spa.Options.SourcePath = "ClientApp"; | |
if (env.IsDevelopment()) | |
{ | |
spa.UseAngularCliServer(npmScript: "start"); | |
} | |
}); |
另外,让我们更新environment.ts。添加appUrl
到环境常量,它应该如下所示:
export const environment = { | |
production: false, | |
appUrl: 'https://localhost:44382/' | |
}; |
export const environment = { | |
production: false, | |
appUrl: 'https://localhost:44382/' | |
}; |
export const environment = { | |
production: false, | |
appUrl: 'https://localhost:44382/' | |
}; |
现在在 Visual Studio 2019 中,按 F5,您的 Angular 应用程序和后端将在 IIS Express 上的同一地址上启动并运行:
是否使用 Node.js 命令提示符来构建和运行 Angular 应用程序完全由您决定。只需记住后端也需要启动并运行即可。
Visual Studio 构建并运行前端和后端意味着您可以少考虑一件事。
创建博客文章模型和服务方法
我们需要一个可以在 TypeScript 中使用的博客文章模型。让我们创建一个名为 models 的新文件夹,然后创建一个 TypeScript 文件(在 VS Code 中右键单击该文件夹 -> 新建文件),并将其命名为blogpost.ts。
将此 BlogPost 模型类复制并粘贴到 blogposts.ts 中:
export class BlogPost { | |
postId?: number; | |
creator: string; | |
title: string; | |
body: string; | |
dt: Date; | |
} |
export class BlogPost { | |
postId?: number; | |
creator: string; | |
title: string; | |
body: string; | |
dt: Date; | |
} |
export class BlogPost { | |
postId?: number; | |
creator: string; | |
title: string; | |
body: string; | |
dt: Date; | |
} |
我们的 BlogPost 模型现在可以在整个应用程序中使用。
Angular 8 服务 CRUD 任务
我们的 Angular 服务将调用我们的后端并执行以下任务:
-
创建博客文章。
-
显示所有博客文章/显示单篇博客文章。
-
更新现有的博客文章。
-
删除博客文章。
现在让我们回到之前创建的服务,它位于 services 文件夹中。打开blog-post.service.ts并编辑该文件,如下所示:
import { Injectable } from '@angular/core'; | |
import { HttpClient, HttpHeaders } from '@angular/common/http'; | |
import { Observable, throwError } from 'rxjs'; | |
import { retry, catchError } from 'rxjs/operators'; | |
import { environment } from 'src/environments/environment'; | |
import { BlogPost } from '../models/blogpost'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class BlogPostService { | |
myAppUrl: string; | |
myApiUrl: string; | |
httpOptions = { | |
headers: new HttpHeaders({ | |
'Content-Type': 'application/json; charset=utf-8' | |
}) | |
}; | |
constructor(private http: HttpClient) { | |
this.myAppUrl = environment.appUrl; | |
this.myApiUrl = 'api/BlogPosts/'; | |
} | |
getBlogPosts(): Observable<BlogPost[]> { | |
return this.http.get<BlogPost[]>(this.myAppUrl + this.myApiUrl) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
getBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.get<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
saveBlogPost(blogPost): Observable<BlogPost> { | |
return this.http.post<BlogPost>(this.myAppUrl + this.myApiUrl, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
updateBlogPost(postId: number, blogPost): Observable<BlogPost> { | |
return this.http.put<BlogPost>(this.myAppUrl + this.myApiUrl + postId, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
deleteBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.delete<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
errorHandler(error) { | |
let errorMessage = ''; | |
if (error.error instanceof ErrorEvent) { | |
// Get client-side error | |
errorMessage = error.error.message; | |
} else { | |
// Get server-side error | |
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; | |
} | |
console.log(errorMessage); | |
return throwError(errorMessage); | |
} | |
} |
import { Injectable } from '@angular/core'; | |
import { HttpClient, HttpHeaders } from '@angular/common/http'; | |
import { Observable, throwError } from 'rxjs'; | |
import { retry, catchError } from 'rxjs/operators'; | |
import { environment } from 'src/environments/environment'; | |
import { BlogPost } from '../models/blogpost'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class BlogPostService { | |
myAppUrl: string; | |
myApiUrl: string; | |
httpOptions = { | |
headers: new HttpHeaders({ | |
'Content-Type': 'application/json; charset=utf-8' | |
}) | |
}; | |
constructor(private http: HttpClient) { | |
this.myAppUrl = environment.appUrl; | |
this.myApiUrl = 'api/BlogPosts/'; | |
} | |
getBlogPosts(): Observable<BlogPost[]> { | |
return this.http.get<BlogPost[]>(this.myAppUrl + this.myApiUrl) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
getBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.get<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
saveBlogPost(blogPost): Observable<BlogPost> { | |
return this.http.post<BlogPost>(this.myAppUrl + this.myApiUrl, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
updateBlogPost(postId: number, blogPost): Observable<BlogPost> { | |
return this.http.put<BlogPost>(this.myAppUrl + this.myApiUrl + postId, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
deleteBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.delete<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
errorHandler(error) { | |
let errorMessage = ''; | |
if (error.error instanceof ErrorEvent) { | |
// Get client-side error | |
errorMessage = error.error.message; | |
} else { | |
// Get server-side error | |
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; | |
} | |
console.log(errorMessage); | |
return throwError(errorMessage); | |
} | |
} |
import { Injectable } from '@angular/core'; | |
import { HttpClient, HttpHeaders } from '@angular/common/http'; | |
import { Observable, throwError } from 'rxjs'; | |
import { retry, catchError } from 'rxjs/operators'; | |
import { environment } from 'src/environments/environment'; | |
import { BlogPost } from '../models/blogpost'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class BlogPostService { | |
myAppUrl: string; | |
myApiUrl: string; | |
httpOptions = { | |
headers: new HttpHeaders({ | |
'Content-Type': 'application/json; charset=utf-8' | |
}) | |
}; | |
constructor(private http: HttpClient) { | |
this.myAppUrl = environment.appUrl; | |
this.myApiUrl = 'api/BlogPosts/'; | |
} | |
getBlogPosts(): Observable<BlogPost[]> { | |
return this.http.get<BlogPost[]>(this.myAppUrl + this.myApiUrl) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
getBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.get<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
saveBlogPost(blogPost): Observable<BlogPost> { | |
return this.http.post<BlogPost>(this.myAppUrl + this.myApiUrl, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
updateBlogPost(postId: number, blogPost): Observable<BlogPost> { | |
return this.http.put<BlogPost>(this.myAppUrl + this.myApiUrl + postId, JSON.stringify(blogPost), this.httpOptions) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
deleteBlogPost(postId: number): Observable<BlogPost> { | |
return this.http.delete<BlogPost>(this.myAppUrl + this.myApiUrl + postId) | |
.pipe( | |
retry(1), | |
catchError(this.errorHandler) | |
); | |
} | |
errorHandler(error) { | |
let errorMessage = ''; | |
if (error.error instanceof ErrorEvent) { | |
// Get client-side error | |
errorMessage = error.error.message; | |
} else { | |
// Get server-side error | |
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; | |
} | |
console.log(errorMessage); | |
return throwError(errorMessage); | |
} | |
} |
我们已经将服务注入到 app.module.ts 中的提供程序数组中,这意味着该服务可以在整个应用程序中立即使用。
Angular 中的可观察对象
Angular HttpClient 的方法使用了RxJS 的可观察对象。可观察对象支持在应用程序中的发布者和订阅者之间传递消息。它们功能强大,且具有诸多优势,因此在 Angular 中被广泛使用。
创建(发布)可观察对象后,我们需要使用 subscribe() 方法来接收通知。之后,我们会得到一个可用的 Subscription 对象。此外,我们还可以使用 unsubscribe() 方法来停止接收通知。
我们通过装饰器使 BlogPostService 可注入@Injectable()
。稍后我们会将该服务注入到我们的组件中。
对于我们服务的 post 和 put 方法,我们将发送application/json
。
然后,我们pipe()
对每个服务调用该方法。在这里,我们可以传入操作函数来转换可观察集合中的数据。我们将retry
和添加catchError
到 pipe 方法中。
在 Angular 中订阅可观察对象非常常见。这没问题,但你也需要记得取消订阅。pipe 会自动帮你取消订阅,释放内存资源并防止泄漏。
更新组件以显示服务数据
接下来介绍我们的三个博客组件。首先从 BlogPostsComponent 开始,它将列出我们所有的博客文章。更新blog-posts.component.ts文件,如下所示:
import { Component, OnInit } from '@angular/core'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-posts', | |
templateUrl: './blog-posts.component.html', | |
styleUrls: ['./blog-posts.component.scss'] | |
}) | |
export class BlogPostsComponent implements OnInit { | |
blogPosts$: Observable<BlogPost[]>; | |
constructor(private blogPostService: BlogPostService) { | |
} | |
ngOnInit() { | |
this.loadBlogPosts(); | |
} | |
loadBlogPosts() { | |
this.blogPosts$ = this.blogPostService.getBlogPosts(); | |
} | |
delete(postId) { | |
const ans = confirm('Do you want to delete blog post with id: ' + postId); | |
if (ans) { | |
this.blogPostService.deleteBlogPost(postId).subscribe((data) => { | |
this.loadBlogPosts(); | |
}); | |
} | |
} | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-posts', | |
templateUrl: './blog-posts.component.html', | |
styleUrls: ['./blog-posts.component.scss'] | |
}) | |
export class BlogPostsComponent implements OnInit { | |
blogPosts$: Observable<BlogPost[]>; | |
constructor(private blogPostService: BlogPostService) { | |
} | |
ngOnInit() { | |
this.loadBlogPosts(); | |
} | |
loadBlogPosts() { | |
this.blogPosts$ = this.blogPostService.getBlogPosts(); | |
} | |
delete(postId) { | |
const ans = confirm('Do you want to delete blog post with id: ' + postId); | |
if (ans) { | |
this.blogPostService.deleteBlogPost(postId).subscribe((data) => { | |
this.loadBlogPosts(); | |
}); | |
} | |
} | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-posts', | |
templateUrl: './blog-posts.component.html', | |
styleUrls: ['./blog-posts.component.scss'] | |
}) | |
export class BlogPostsComponent implements OnInit { | |
blogPosts$: Observable<BlogPost[]>; | |
constructor(private blogPostService: BlogPostService) { | |
} | |
ngOnInit() { | |
this.loadBlogPosts(); | |
} | |
loadBlogPosts() { | |
this.blogPosts$ = this.blogPostService.getBlogPosts(); | |
} | |
delete(postId) { | |
const ans = confirm('Do you want to delete blog post with id: ' + postId); | |
if (ans) { | |
this.blogPostService.deleteBlogPost(postId).subscribe((data) => { | |
this.loadBlogPosts(); | |
}); | |
} | |
} | |
} |
我们在构造函数中注入依赖BlogPostService
,然后loadBlogPosts()
简单地调用我们的 Angular 服务。
由于服务getBlogPosts()
方法提供了Observable<BlogPost[]>
返回值,我们将其赋值给该组件的blogPost$
对象。通常的做法是在可观察对象末尾添加 $ 符号。
在该delete()
方法中,我们需要订阅我们的可观察对象来执行操作,然后重新加载博客文章列表。
现在打开blog-posts.component.html并将其更新为如下所示:
<h1>Blog posts</h1> | |
<p *ngIf="!(blogPosts$ | async)"><em>Loading...</em></p> | |
<p> | |
<a [routerLink]="['/add']" class="btn btn-primary float-right mb-3">New post</a> | |
</p> | |
<table class="table table-sm table-hover" *ngIf="(blogPosts$ | async)?.length>0"> | |
<thead> | |
<tr> | |
<th>#</th> | |
<th>Title</th> | |
<th>Creator</th> | |
<th>Date</th> | |
<th></th> | |
<th></th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr *ngFor="let blogPost of (blogPosts$ | async)"> | |
<td>{{ blogPost.postId }}</td> | |
<td><a [routerLink]="['/blogpost/', blogPost.postId]">{{ blogPost.title }}</a></td> | |
<td>{{ blogPost.creator }}</td> | |
<td>{{ blogPost.dt | date: "dd.MM.y" }}</td> | |
<td><a [routerLink]="['/blogpost/edit/', blogPost.postId]" class="btn btn-primary btn-sm float-right">Edit</a></td> | |
<td><a [routerLink]="" (click)="delete(blogPost.postId)" class="btn btn-danger btn-sm float-right">Delete</a></td> | |
</tr> | |
</tbody> | |
</table> |
<h1>Blog posts</h1> | |
<p *ngIf="!(blogPosts$ | async)"><em>Loading...</em></p> | |
<p> | |
<a [routerLink]="['/add']" class="btn btn-primary float-right mb-3">New post</a> | |
</p> | |
<table class="table table-sm table-hover" *ngIf="(blogPosts$ | async)?.length>0"> | |
<thead> | |
<tr> | |
<th>#</th> | |
<th>Title</th> | |
<th>Creator</th> | |
<th>Date</th> | |
<th></th> | |
<th></th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr *ngFor="let blogPost of (blogPosts$ | async)"> | |
<td>{{ blogPost.postId }}</td> | |
<td><a [routerLink]="['/blogpost/', blogPost.postId]">{{ blogPost.title }}</a></td> | |
<td>{{ blogPost.creator }}</td> | |
<td>{{ blogPost.dt | date: "dd.MM.y" }}</td> | |
<td><a [routerLink]="['/blogpost/edit/', blogPost.postId]" class="btn btn-primary btn-sm float-right">Edit</a></td> | |
<td><a [routerLink]="" (click)="delete(blogPost.postId)" class="btn btn-danger btn-sm float-right">Delete</a></td> | |
</tr> | |
</tbody> | |
</table> |
<h1>Blog posts</h1> | |
<p *ngIf="!(blogPosts$ | async)"><em>Loading...</em></p> | |
<p> | |
<a [routerLink]="['/add']" class="btn btn-primary float-right mb-3">New post</a> | |
</p> | |
<table class="table table-sm table-hover" *ngIf="(blogPosts$ | async)?.length>0"> | |
<thead> | |
<tr> | |
<th>#</th> | |
<th>Title</th> | |
<th>Creator</th> | |
<th>Date</th> | |
<th></th> | |
<th></th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr *ngFor="let blogPost of (blogPosts$ | async)"> | |
<td>{{ blogPost.postId }}</td> | |
<td><a [routerLink]="['/blogpost/', blogPost.postId]">{{ blogPost.title }}</a></td> | |
<td>{{ blogPost.creator }}</td> | |
<td>{{ blogPost.dt | date: "dd.MM.y" }}</td> | |
<td><a [routerLink]="['/blogpost/edit/', blogPost.postId]" class="btn btn-primary btn-sm float-right">Edit</a></td> | |
<td><a [routerLink]="" (click)="delete(blogPost.postId)" class="btn btn-danger btn-sm float-right">Delete</a></td> | |
</tr> | |
</tbody> | |
</table> |
我们使用AsyncPipe
来订阅我们的可观察对象。当我们想在 HTML 模板文件中显示可观察值时,我们使用以下语法:
(blogPosts$ | async)
ngIf
和ngFor
是通过添加或删除元素来改变 DOM 结构的结构指令。
该routerLink
指令让我们可以链接到应用程序中的特定路线。
您可以在 Visual Studio 2019 中按 F5 或使用 Node.js 命令提示符来ng serve
启动该应用。如果您使用 Node.js 启动该应用,请确保后端也在后台启动(使用 Visual Studio F5 命令)。
由于我们之前已经在 Postman 中手动添加了博客文章,因此我们现在应该看到以下内容:
出色的!
接下来是blog-post.component.ts,用于查看单篇博客文章。编辑该文件如下:
import { Component, OnInit } from '@angular/core'; | |
import { ActivatedRoute } from '@angular/router'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post', | |
templateUrl: './blog-post.component.html', | |
styleUrls: ['./blog-post.component.scss'] | |
}) | |
export class BlogPostComponent implements OnInit { | |
blogPost$: Observable<BlogPost>; | |
postId: number; | |
constructor(private blogPostService: BlogPostService, private avRoute: ActivatedRoute) { | |
const idParam = 'id'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
} | |
ngOnInit() { | |
this.loadBlogPost(); | |
} | |
loadBlogPost() { | |
this.blogPost$ = this.blogPostService.getBlogPost(this.postId); | |
} | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { ActivatedRoute } from '@angular/router'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post', | |
templateUrl: './blog-post.component.html', | |
styleUrls: ['./blog-post.component.scss'] | |
}) | |
export class BlogPostComponent implements OnInit { | |
blogPost$: Observable<BlogPost>; | |
postId: number; | |
constructor(private blogPostService: BlogPostService, private avRoute: ActivatedRoute) { | |
const idParam = 'id'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
} | |
ngOnInit() { | |
this.loadBlogPost(); | |
} | |
loadBlogPost() { | |
this.blogPost$ = this.blogPostService.getBlogPost(this.postId); | |
} | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { ActivatedRoute } from '@angular/router'; | |
import { Observable } from 'rxjs'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post', | |
templateUrl: './blog-post.component.html', | |
styleUrls: ['./blog-post.component.scss'] | |
}) | |
export class BlogPostComponent implements OnInit { | |
blogPost$: Observable<BlogPost>; | |
postId: number; | |
constructor(private blogPostService: BlogPostService, private avRoute: ActivatedRoute) { | |
const idParam = 'id'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
} | |
ngOnInit() { | |
this.loadBlogPost(); | |
} | |
loadBlogPost() { | |
this.blogPost$ = this.blogPostService.getBlogPost(this.postId); | |
} | |
} |
由于我们想要显示的是一篇博客文章,因此我们使用内置ActivatedRoute
组件从 url 查询字符串中获取 id 键,并将其传递给服务getBlogPost()
方法。
现在打开blog-post.component.html并将其编辑为如下所示:
<ng-container *ngIf="(blogPost$ | async) as blogPost; else loading"> | |
<h1>{{ blogPost.title }}</h1> | |
<div>{{ blogPost.body }}</div> | |
<ul> | |
<li>{{ blogPost.creator }}</li> | |
<li>{{ blogPost.dt }}</li> | |
</ul> | |
</ng-container> | |
<ng-template #loading>Loading…</ng-template> |
<ng-container *ngIf="(blogPost$ | async) as blogPost; else loading"> | |
<h1>{{ blogPost.title }}</h1> | |
<div>{{ blogPost.body }}</div> | |
<ul> | |
<li>{{ blogPost.creator }}</li> | |
<li>{{ blogPost.dt }}</li> | |
</ul> | |
</ng-container> | |
<ng-template #loading>Loading…</ng-template> |
<ng-container *ngIf="(blogPost$ | async) as blogPost; else loading"> | |
<h1>{{ blogPost.title }}</h1> | |
<div>{{ blogPost.body }}</div> | |
<ul> | |
<li>{{ blogPost.creator }}</li> | |
<li>{{ blogPost.dt }}</li> | |
</ul> | |
</ng-container> | |
<ng-template #loading>Loading…</ng-template> |
我们AsyncPipe
再次使用了 ,并且还使用了别名,blogPost
这样我们就不必blogPost | async
在访问 blogPost 属性的每个地方都写代码了。我们还提供了一个加载屏幕。
越来越近了。现在我们只需要一种创建新博客文章和编辑现有文章的方法。打开blog-post-add-edit.component.ts并将其编辑如下:
import { Component, OnInit } from '@angular/core'; | |
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
import { Router, ActivatedRoute } from '@angular/router'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post-add-edit', | |
templateUrl: './blog-post-add-edit.component.html', | |
styleUrls: ['./blog-post-add-edit.component.scss'] | |
}) | |
export class BlogPostAddEditComponent implements OnInit { | |
form: FormGroup; | |
actionType: string; | |
formTitle: string; | |
formBody: string; | |
postId: number; | |
errorMessage: any; | |
existingBlogPost: BlogPost; | |
constructor(private blogPostService: BlogPostService, private formBuilder: FormBuilder, private avRoute: ActivatedRoute, private router: Router) { | |
const idParam = 'id'; | |
this.actionType = 'Add'; | |
this.formTitle = 'title'; | |
this.formBody = 'body'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
this.form = this.formBuilder.group( | |
{ | |
postId: 0, | |
title: ['', [Validators.required]], | |
body: ['', [Validators.required]], | |
} | |
) | |
} | |
ngOnInit() { | |
if (this.postId > 0) { | |
this.actionType = 'Edit'; | |
this.blogPostService.getBlogPost(this.postId) | |
.subscribe(data => ( | |
this.existingBlogPost = data, | |
this.form.controls[this.formTitle].setValue(data.title), | |
this.form.controls[this.formBody].setValue(data.body) | |
)); | |
} | |
} | |
save() { | |
if (!this.form.valid) { | |
return; | |
} | |
if (this.actionType === 'Add') { | |
let blogPost: BlogPost = { | |
dt: new Date(), | |
creator: 'Martin', | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.saveBlogPost(blogPost) | |
.subscribe((data) => { | |
this.router.navigate(['/blogpost', data.postId]); | |
}); | |
} | |
if (this.actionType === 'Edit') { | |
let blogPost: BlogPost = { | |
postId: this.existingBlogPost.postId, | |
dt: this.existingBlogPost.dt, | |
creator: this.existingBlogPost.creator, | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.updateBlogPost(blogPost.postId, blogPost) | |
.subscribe((data) => { | |
this.router.navigate([this.router.url]); | |
}); | |
} | |
} | |
cancel() { | |
this.router.navigate(['/']); | |
} | |
get title() { return this.form.get(this.formTitle); } | |
get body() { return this.form.get(this.formBody); } | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
import { Router, ActivatedRoute } from '@angular/router'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post-add-edit', | |
templateUrl: './blog-post-add-edit.component.html', | |
styleUrls: ['./blog-post-add-edit.component.scss'] | |
}) | |
export class BlogPostAddEditComponent implements OnInit { | |
form: FormGroup; | |
actionType: string; | |
formTitle: string; | |
formBody: string; | |
postId: number; | |
errorMessage: any; | |
existingBlogPost: BlogPost; | |
constructor(private blogPostService: BlogPostService, private formBuilder: FormBuilder, private avRoute: ActivatedRoute, private router: Router) { | |
const idParam = 'id'; | |
this.actionType = 'Add'; | |
this.formTitle = 'title'; | |
this.formBody = 'body'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
this.form = this.formBuilder.group( | |
{ | |
postId: 0, | |
title: ['', [Validators.required]], | |
body: ['', [Validators.required]], | |
} | |
) | |
} | |
ngOnInit() { | |
if (this.postId > 0) { | |
this.actionType = 'Edit'; | |
this.blogPostService.getBlogPost(this.postId) | |
.subscribe(data => ( | |
this.existingBlogPost = data, | |
this.form.controls[this.formTitle].setValue(data.title), | |
this.form.controls[this.formBody].setValue(data.body) | |
)); | |
} | |
} | |
save() { | |
if (!this.form.valid) { | |
return; | |
} | |
if (this.actionType === 'Add') { | |
let blogPost: BlogPost = { | |
dt: new Date(), | |
creator: 'Martin', | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.saveBlogPost(blogPost) | |
.subscribe((data) => { | |
this.router.navigate(['/blogpost', data.postId]); | |
}); | |
} | |
if (this.actionType === 'Edit') { | |
let blogPost: BlogPost = { | |
postId: this.existingBlogPost.postId, | |
dt: this.existingBlogPost.dt, | |
creator: this.existingBlogPost.creator, | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.updateBlogPost(blogPost.postId, blogPost) | |
.subscribe((data) => { | |
this.router.navigate([this.router.url]); | |
}); | |
} | |
} | |
cancel() { | |
this.router.navigate(['/']); | |
} | |
get title() { return this.form.get(this.formTitle); } | |
get body() { return this.form.get(this.formBody); } | |
} |
import { Component, OnInit } from '@angular/core'; | |
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
import { Router, ActivatedRoute } from '@angular/router'; | |
import { BlogPostService } from '../services/blog-post.service'; | |
import { BlogPost } from '../models/blogpost'; | |
@Component({ | |
selector: 'app-blog-post-add-edit', | |
templateUrl: './blog-post-add-edit.component.html', | |
styleUrls: ['./blog-post-add-edit.component.scss'] | |
}) | |
export class BlogPostAddEditComponent implements OnInit { | |
form: FormGroup; | |
actionType: string; | |
formTitle: string; | |
formBody: string; | |
postId: number; | |
errorMessage: any; | |
existingBlogPost: BlogPost; | |
constructor(private blogPostService: BlogPostService, private formBuilder: FormBuilder, private avRoute: ActivatedRoute, private router: Router) { | |
const idParam = 'id'; | |
this.actionType = 'Add'; | |
this.formTitle = 'title'; | |
this.formBody = 'body'; | |
if (this.avRoute.snapshot.params[idParam]) { | |
this.postId = this.avRoute.snapshot.params[idParam]; | |
} | |
this.form = this.formBuilder.group( | |
{ | |
postId: 0, | |
title: ['', [Validators.required]], | |
body: ['', [Validators.required]], | |
} | |
) | |
} | |
ngOnInit() { | |
if (this.postId > 0) { | |
this.actionType = 'Edit'; | |
this.blogPostService.getBlogPost(this.postId) | |
.subscribe(data => ( | |
this.existingBlogPost = data, | |
this.form.controls[this.formTitle].setValue(data.title), | |
this.form.controls[this.formBody].setValue(data.body) | |
)); | |
} | |
} | |
save() { | |
if (!this.form.valid) { | |
return; | |
} | |
if (this.actionType === 'Add') { | |
let blogPost: BlogPost = { | |
dt: new Date(), | |
creator: 'Martin', | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.saveBlogPost(blogPost) | |
.subscribe((data) => { | |
this.router.navigate(['/blogpost', data.postId]); | |
}); | |
} | |
if (this.actionType === 'Edit') { | |
let blogPost: BlogPost = { | |
postId: this.existingBlogPost.postId, | |
dt: this.existingBlogPost.dt, | |
creator: this.existingBlogPost.creator, | |
title: this.form.get(this.formTitle).value, | |
body: this.form.get(this.formBody).value | |
}; | |
this.blogPostService.updateBlogPost(blogPost.postId, blogPost) | |
.subscribe((data) => { | |
this.router.navigate([this.router.url]); | |
}); | |
} | |
} | |
cancel() { | |
this.router.navigate(['/']); | |
} | |
get title() { return this.form.get(this.formTitle); } | |
get body() { return this.form.get(this.formBody); } | |
} |
这里我们介绍 Angular 形式:FormBuilder
,FormGroup
以及Validators
。
根据我们是创建新的博客文章还是编辑现有的文章,我们使用它actionType
来显示包含或不包含数据的正确表单视图。当我们保存或更新博客文章时,我们会创建一个新的BlogPost
对象,然后向其中填充正确的表单数据,并将其发布到我们的服务中。
让我们打开blog-post-add-edit.component.html并将其编辑成如下所示:
<h1>{{actionType}} blog post</h1> | |
<form [formGroup]="form" (ngSubmit)="save()" #formDir="ngForm" novalidate> | |
<div class="form-group row"> | |
<label class=" control-label col-md-12">Title</label> | |
<div class="col-md-12"> | |
<input class="form-control" type="text" formControlName="title"> | |
</div> | |
<span class="text-danger ml-3" *ngIf="title.invalid && formDir.submitted"> | |
Title is required. | |
</span> | |
</div> | |
<div class="form-group row"> | |
<label class="control-label col-md-12" for="Body">Body text</label> | |
<div class="col-md-12"> | |
<textarea class="form-control" rows="15" formControlName="body"></textarea> | |
</div> | |
<span class="text-danger ml-3" *ngIf="body.invalid && formDir.submitted"> | |
Body is required. | |
</span> | |
</div> | |
<div class="form-group"> | |
<button type="submit" class="btn btn-success float-right">Save</button> | |
<button class="btn btn-secondary float-left" (click)="cancel()">Cancel</button> | |
</div> | |
</form> |
<h1>{{actionType}} blog post</h1> | |
<form [formGroup]="form" (ngSubmit)="save()" #formDir="ngForm" novalidate> | |
<div class="form-group row"> | |
<label class=" control-label col-md-12">Title</label> | |
<div class="col-md-12"> | |
<input class="form-control" type="text" formControlName="title"> | |
</div> | |
<span class="text-danger ml-3" *ngIf="title.invalid && formDir.submitted"> | |
Title is required. | |
</span> | |
</div> | |
<div class="form-group row"> | |
<label class="control-label col-md-12" for="Body">Body text</label> | |
<div class="col-md-12"> | |
<textarea class="form-control" rows="15" formControlName="body"></textarea> | |
</div> | |
<span class="text-danger ml-3" *ngIf="body.invalid && formDir.submitted"> | |
Body is required. | |
</span> | |
</div> | |
<div class="form-group"> | |
<button type="submit" class="btn btn-success float-right">Save</button> | |
<button class="btn btn-secondary float-left" (click)="cancel()">Cancel</button> | |
</div> | |
</form> |
<h1>{{actionType}} blog post</h1> | |
<form [formGroup]="form" (ngSubmit)="save()" #formDir="ngForm" novalidate> | |
<div class="form-group row"> | |
<label class=" control-label col-md-12">Title</label> | |
<div class="col-md-12"> | |
<input class="form-control" type="text" formControlName="title"> | |
</div> | |
<span class="text-danger ml-3" *ngIf="title.invalid && formDir.submitted"> | |
Title is required. | |
</span> | |
</div> | |
<div class="form-group row"> | |
<label class="control-label col-md-12" for="Body">Body text</label> | |
<div class="col-md-12"> | |
<textarea class="form-control" rows="15" formControlName="body"></textarea> | |
</div> | |
<span class="text-danger ml-3" *ngIf="body.invalid && formDir.submitted"> | |
Body is required. | |
</span> | |
</div> | |
<div class="form-group"> | |
<button type="submit" class="btn btn-success float-right">Save</button> | |
<button class="btn btn-secondary float-left" (click)="cancel()">Cancel</button> | |
</div> | |
</form> |
这是经过验证的表格。
完成了!
在 Visual Studio 2019 中按 F5 或使用 Node.js 命令提示符来ng serve
浏览最终的应用程序。(如果您使用 Node.js 启动应用程序,请确保后端也在后台启动(使用 Visual Studio F5 命令))