@ -0,0 +1,661 @@ |
|||||
|
## Tutorial Angular - Parte I |
||||
|
|
||||
|
### Sobre este tutorial |
||||
|
|
||||
|
Nesta série de tutoriais, você criará um aplicativo usado para gerenciar uma lista de livros e seus autores. **Angular** será usado como estrutura da interface do usuário e **MongoDB** será usado como provedor de banco de dados. |
||||
|
|
||||
|
Esta é a primeira parte da série de tutoriais angulares. Veja todas as peças: |
||||
|
|
||||
|
- **Parte I: Crie o projeto e uma página de lista de livros (este tutorial)** |
||||
|
- [Parte II: Criar, atualizar e excluir livros](Part-II) |
||||
|
- [Parte III: Testes de Integração](Part-III) |
||||
|
|
||||
|
Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb) . |
||||
|
|
||||
|
### Criando o projeto |
||||
|
|
||||
|
Crie um novo projeto nomeado `Acme.BookStore`selecionando Angular como a estrutura da interface do usuário e MongoDB como o provedor de banco de dados, crie o banco de dados e execute o aplicativo seguindo o [documento Introdução](../../Getting-Started-Angular-Template.md) . |
||||
|
|
||||
|
### Estrutura da solução (back-end) |
||||
|
|
||||
|
É assim que a estrutura da solução em camadas cuida da criação: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> Você pode ver o [documento do modelo de aplicativo](../../Startup-Templates/Application.md) para entender a estrutura da solução em detalhes. No entanto, você entenderá o básico com este tutorial. |
||||
|
|
||||
|
### Criar a entidade do livro |
||||
|
|
||||
|
A camada de domínio no modelo de inicialização é separada em dois projetos: |
||||
|
|
||||
|
- `Acme.BookStore.Domain`contém suas [entidades](../../Entities.md) , [serviços de domínio](../../Domain-Services.md) e outros objetos principais de domínio. |
||||
|
- `Acme.BookStore.Domain.Shared` contém constantes, enumerações ou outros objetos relacionados ao domínio que podem ser compartilhados com os clientes. |
||||
|
|
||||
|
Defina [entidades](../../Entities.md) na **camada de domínio** ( `Acme.BookStore.Domain`projeto) da solução. A entidade principal do aplicativo é a `Book`. Crie uma classe, chamada `Book`, no `Acme.BookStore.Domain`projeto, como mostrado abaixo: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using Volo.Abp.Domain.Entities.Auditing; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class Book : AuditedAggregateRoot<Guid> |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public BookType Type { get; set; } |
||||
|
|
||||
|
public DateTime PublishDate { get; set; } |
||||
|
|
||||
|
public float Price { get; set; } |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- O ABP possui duas classes base fundamentais para entidades: `AggregateRoot`e `Entity`. **A raiz agregada** é um dos conceitos de **DDD (Domain Driven Design)** . Consulte o [documento da entidade](../../Entities.md) para obter detalhes e melhores práticas. |
||||
|
- `Book`entidade herda `AuditedAggregateRoot`que adiciona algumas propriedades de auditoria ( `CreationTime`, `CreatorId`, `LastModificationTime`... etc.) no topo da `AggregateRoot`classe. |
||||
|
- `Guid`é o **tipo** de **chave primária** da `Book`entidade. |
||||
|
|
||||
|
#### BookType Enum |
||||
|
|
||||
|
Defina a `BookType`enumeração no `Acme.BookStore.Domain.Shared`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public enum BookType |
||||
|
{ |
||||
|
Undefined, |
||||
|
Adventure, |
||||
|
Biography, |
||||
|
Dystopia, |
||||
|
Fantastic, |
||||
|
Horror, |
||||
|
Science, |
||||
|
ScienceFiction, |
||||
|
Poetry |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Adicionar entidade de livro ao seu DbContext |
||||
|
|
||||
|
Adicione uma `IMongoCollection`propriedade ao `BookStoreMongoDbContext`interior do `Acme.BookStore.MongoDB`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
public class BookStoreMongoDbContext : AbpMongoDbContext |
||||
|
{ |
||||
|
public IMongoCollection<Book> Books => Collection<Book>(); |
||||
|
... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Adicionar dados de semente (amostra) |
||||
|
|
||||
|
Esta seção é opcional, mas seria bom ter um dado inicial no banco de dados na primeira execução. O ABP fornece um [sistema de semente de dados](../../Data-Seeding.md) . Crie uma classe derivada de `IDataSeedContributor`no `.Domain`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookStoreDataSeederContributor |
||||
|
: IDataSeedContributor, ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
|
||||
|
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task SeedAsync(DataSeedContext context) |
||||
|
{ |
||||
|
if (await _bookRepository.GetCountAsync() > 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Name = "1984", |
||||
|
Type = BookType.Dystopia, |
||||
|
PublishDate = new DateTime(1949, 6, 8), |
||||
|
Price = 19.84f |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Name = "The Hitchhiker's Guide to the Galaxy", |
||||
|
Type = BookType.ScienceFiction, |
||||
|
PublishDate = new DateTime(1995, 9, 27), |
||||
|
Price = 42.0f |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`BookStoreDataSeederContributor`simplesmente insere dois livros no banco de dados se não houver nenhum livro adicionado antes. O ABP descobre e executa automaticamente essa classe quando você propaga o banco de dados executando o `Acme.BookStore.DbMigrator`projeto. |
||||
|
|
||||
|
### Crie o serviço de aplicativo |
||||
|
|
||||
|
O próximo passo é criar um [serviço de aplicativo](../../Application-Services.md) para gerenciar (criar, listar, atualizar, excluir ...) os livros. A camada de aplicativo no modelo de inicialização é separada em dois projetos: |
||||
|
|
||||
|
- `Acme.BookStore.Application.Contracts` contém principalmente seus DTOs e interfaces de serviço de aplicativo. |
||||
|
- `Acme.BookStore.Application` contém as implementações dos seus serviços de aplicativo. |
||||
|
|
||||
|
#### BookDto |
||||
|
|
||||
|
Crie uma classe DTO denominada `BookDto`no `Acme.BookStore.Application.Contracts`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookDto : AuditedEntityDto<Guid> |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public BookType Type { get; set; } |
||||
|
|
||||
|
public DateTime PublishDate { get; set; } |
||||
|
|
||||
|
public float Price { get; set; } |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- **As** classes **DTO** são usadas para **transferir dados** entre a *camada de apresentação* e a *camada de aplicativo* . Consulte o [documento Objetos de transferência de dados](../../Data-Transfer-Objects.md) para obter mais detalhes. |
||||
|
- `BookDto` é usado para transferir dados do livro para a camada de apresentação para mostrar as informações do livro na interface do usuário. |
||||
|
- `BookDto`é derivado do `AuditedEntityDto<Guid>`que possui propriedades de auditoria exatamente como a `Book`classe definida acima. |
||||
|
|
||||
|
Será necessário converter `Book`entidades em `BookDto`objetos enquanto retorna os livros para a camada de apresentação. A biblioteca do [AutoMapper](https://automapper.org/) pode automatizar essa conversão quando você define o mapeamento adequado. O modelo de inicialização é fornecido com o AutoMapper configurado, para que você possa definir o mapeamento na `BookStoreApplicationAutoMapperProfile`classe no `Acme.BookStore.Application`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using AutoMapper; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookStoreApplicationAutoMapperProfile : Profile |
||||
|
{ |
||||
|
public BookStoreApplicationAutoMapperProfile() |
||||
|
{ |
||||
|
CreateMap<Book, BookDto>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### CreateUpdateBookDto |
||||
|
|
||||
|
Crie uma classe DTO denominada `CreateUpdateBookDto`no `Acme.BookStore.Application.Contracts`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class CreateUpdateBookDto |
||||
|
{ |
||||
|
[Required] |
||||
|
[StringLength(128)] |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
public BookType Type { get; set; } = BookType.Undefined; |
||||
|
|
||||
|
[Required] |
||||
|
public DateTime PublishDate { get; set; } |
||||
|
|
||||
|
[Required] |
||||
|
public float Price { get; set; } |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Essa classe DTO é usada para obter informações do livro a partir da interface do usuário ao criar ou atualizar um livro. |
||||
|
- Ele define atributos de anotação de dados (como `[Required]`) para definir validações para as propriedades. Os DTOs são [validados automaticamente](../../Validation.md) pela estrutura ABP. |
||||
|
|
||||
|
Em seguida, adicione um mapeamento `BookStoreApplicationAutoMapperProfile`do `CreateUpdateBookDto`objeto à `Book`entidade: |
||||
|
|
||||
|
```csharp |
||||
|
CreateMap<CreateUpdateBookDto, Book>(); |
||||
|
``` |
||||
|
|
||||
|
#### IBookAppService |
||||
|
|
||||
|
Defina uma interface nomeada `IBookAppService`no `Acme.BookStore.Application.Contracts`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public interface IBookAppService : |
||||
|
ICrudAppService< //Defines CRUD methods |
||||
|
BookDto, //Used to show books |
||||
|
Guid, //Primary key of the book entity |
||||
|
PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books |
||||
|
CreateUpdateBookDto, //Used to create a new book |
||||
|
CreateUpdateBookDto> //Used to update a book |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- A definição de interfaces para serviços de aplicativos não é requerida pela estrutura. No entanto, é sugerido como uma prática recomendada. |
||||
|
- `ICrudAppService`define comuns **CRUD** métodos: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync`e `DeleteAsync`. Não é necessário estendê-lo. Em vez disso, você pode herdar da `IApplicationService`interface vazia e definir seus próprios métodos manualmente. |
||||
|
- Existem algumas variações de `ICrudAppService`onde você pode usar DTOs separados para cada método. |
||||
|
|
||||
|
#### BookAppService |
||||
|
|
||||
|
Implemente `IBookAppService`como nomeado `BookAppService`no `Acme.BookStore.Application`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookAppService : |
||||
|
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, |
||||
|
CreateUpdateBookDto, CreateUpdateBookDto>, |
||||
|
IBookAppService |
||||
|
{ |
||||
|
public BookAppService(IRepository<Book, Guid> repository) |
||||
|
: base(repository) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `BookAppService`é derivado do `CrudAppService<...>`qual implementa todos os métodos CRUD definidos acima. |
||||
|
- `BookAppService`injeta `IRepository<Book, Guid>`qual é o repositório padrão da `Book`entidade. O ABP cria automaticamente repositórios padrão para cada raiz (ou entidade) agregada. Veja o [documento do repositório](../../Repositories) . |
||||
|
- `BookAppService`usa `IObjectMapper`para converter `Book`objetos em `BookDto`objetos e `CreateUpdateBookDto`objetos em `Book`objetos. O modelo de inicialização usa a biblioteca [AutoMapper](http://automapper.org/) como o provedor de mapeamento de objetos. Você definiu os mapeamentos antes, para que funcionem conforme o esperado. |
||||
|
|
||||
|
### Controladores de API automática |
||||
|
|
||||
|
Você normalmente cria **controladores** para expor serviços de aplicativos como pontos de extremidade da **API HTTP** . Assim, permite que navegadores ou clientes de terceiros os chamem via AJAX. O ABP pode configurar [**automaticamente**](../../AspNetCore/Auto-API-Controllers.md) seus serviços de aplicativo como controladores de API MVC por convenção. |
||||
|
|
||||
|
#### UI do Swagger |
||||
|
|
||||
|
O modelo de inicialização está configurado para executar a [interface do usuário do swagger](https://swagger.io/tools/swagger-ui/) usando a biblioteca [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) . Execute o `Acme.BookStore.HttpApi.Host`aplicativo e insira `https://localhost:XXXX/swagger/`(substitua XXXX por sua própria porta) como URL no seu navegador. |
||||
|
|
||||
|
Você verá alguns pontos de extremidade de serviço internos, bem como o `Book`serviço e seus pontos de extremidade no estilo REST: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
O Swagger tem uma ótima interface para testar APIs. Você pode tentar executar a `[GET] /api/app/book`API para obter uma lista de livros. |
||||
|
|
||||
|
### Crie a página de livros |
||||
|
|
||||
|
Neste tutorial; |
||||
|
|
||||
|
- [A CLI angular](https://angular.io/cli) será usada para criar módulos, componentes e serviços |
||||
|
- [NGXS](https://ngxs.gitbook.io/ngxs/) será usado como a biblioteca de gerenciamento de estado |
||||
|
- [O Bootstrap](https://ng-bootstrap.github.io/#/home) será usado como a biblioteca de componentes da interface do usuário. |
||||
|
- [O Visual Studio Code](https://code.visualstudio.com/) será usado como editor de código (você pode usar seu editor favorito). |
||||
|
|
||||
|
#### Instalar pacotes NPM |
||||
|
|
||||
|
Abra uma janela do terminal, vá para a `angular`pasta e execute o `yarn` comando para instalar os pacotes NPM: |
||||
|
|
||||
|
``` |
||||
|
yarn |
||||
|
``` |
||||
|
|
||||
|
#### BooksModule |
||||
|
|
||||
|
Execute a seguinte linha de comando para criar um novo módulo, denominado `BooksModule`: |
||||
|
|
||||
|
```bash |
||||
|
yarn ng generate module books --route books --module app.module |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Execute `yarn start`, aguarde Angular para executar o aplicativo e abra `http://localhost:4200/books`em um navegador: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
#### Encaminhamento |
||||
|
|
||||
|
Abra `app-routing.module.ts`e substitua `books`conforme mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic';- |
||||
|
|
||||
|
//... |
||||
|
{ |
||||
|
path: 'books', |
||||
|
component: ApplicationLayoutComponent, |
||||
|
loadChildren: () => import('./books/books.module').then(m => m.BooksModule), |
||||
|
data: { |
||||
|
routes: { |
||||
|
name: 'Books', |
||||
|
} as ABP.Route, |
||||
|
}, |
||||
|
}, |
||||
|
``` |
||||
|
|
||||
|
`ApplicationLayoutComponent`configuração define o layout do aplicativo para a nova página. Se você deseja ver sua rota na barra de navegação (menu principal), também deve adicionar o `data`objeto com `name`propriedade à sua rota. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
#### Componente da lista de livros |
||||
|
|
||||
|
Primeiro, substitua pela `books.component.html`seguinte linha para colocar a saída do roteador: |
||||
|
|
||||
|
```html |
||||
|
<router-outlet></router-outlet> |
||||
|
``` |
||||
|
|
||||
|
Em seguida, execute o comando abaixo no terminal na pasta raiz para gerar um novo componente, chamado book-list: |
||||
|
|
||||
|
```bash |
||||
|
yarn ng generate component books/book-list |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Importe `SharedModule`para `BooksModule`para reutilizar alguns componentes e serviços definidos em: |
||||
|
|
||||
|
```js |
||||
|
import { SharedModule } from '../shared/shared.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
//... |
||||
|
imports: [ |
||||
|
//... |
||||
|
SharedModule, |
||||
|
], |
||||
|
}) |
||||
|
export class BooksModule {} |
||||
|
``` |
||||
|
|
||||
|
Em seguida, atualize o `routes`no `books-routing.module.ts`para adicionar o novo componente book-list: |
||||
|
|
||||
|
```js |
||||
|
import { BookListComponent } from './book-list/book-list.component'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: BooksComponent, |
||||
|
children: [{ path: '', component: BookListComponent }], |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule], |
||||
|
}) |
||||
|
export class BooksRoutingModule {} |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
#### Criar BooksState |
||||
|
|
||||
|
Execute o seguinte comando no terminal para criar um novo estado, denominado `BooksState`: |
||||
|
|
||||
|
```shell |
||||
|
yarn ng generate ngxs-schematic:state books |
||||
|
``` |
||||
|
|
||||
|
Este comando cria vários novos arquivos e edições `app.modules.ts`para importar o `NgxsModule`com o novo estado: |
||||
|
|
||||
|
```js |
||||
|
// app.module.ts |
||||
|
|
||||
|
import { BooksState } from './store/states/books.state'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
//... |
||||
|
NgxsModule.forRoot([BooksState]), |
||||
|
], |
||||
|
//... |
||||
|
}) |
||||
|
export class AppModule {} |
||||
|
``` |
||||
|
|
||||
|
#### Obter dados de livros do back-end |
||||
|
|
||||
|
Primeiro, crie tipos de dados para mapear os dados que retornam do back-end (você pode verificar a interface do swagger ou a API do back-end para conhecer o formato dos dados). |
||||
|
|
||||
|
Modifique o `books.ts`como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
export namespace Books { |
||||
|
export interface State { |
||||
|
books: Response; |
||||
|
} |
||||
|
|
||||
|
export interface Response { |
||||
|
items: Book[]; |
||||
|
totalCount: number; |
||||
|
} |
||||
|
|
||||
|
export interface Book { |
||||
|
name: string; |
||||
|
type: BookType; |
||||
|
publishDate: string; |
||||
|
price: number; |
||||
|
lastModificationTime: string; |
||||
|
lastModifierId: string; |
||||
|
creationTime: string; |
||||
|
creatorId: string; |
||||
|
id: string; |
||||
|
} |
||||
|
|
||||
|
export enum BookType { |
||||
|
Undefined, |
||||
|
Adventure, |
||||
|
Biography, |
||||
|
Dystopia, |
||||
|
Fantastic, |
||||
|
Horror, |
||||
|
Science, |
||||
|
ScienceFiction, |
||||
|
Poetry, |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicionada `Book`interface que representa um objeto de livro e `BookType`enum representa uma categoria de livro. |
||||
|
|
||||
|
#### BooksService |
||||
|
|
||||
|
Agora, crie um novo serviço, nomeado `BooksService`para executar chamadas HTTP para o servidor: |
||||
|
|
||||
|
```bash |
||||
|
yarn ng generate service books/shared/books |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Modifique `books.service.ts`como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { RestService } from '@abp/ng.core'; |
||||
|
import { Books } from '../../store/models'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root', |
||||
|
}) |
||||
|
export class BooksService { |
||||
|
constructor(private restService: RestService) {} |
||||
|
|
||||
|
get(): Observable<Books.Response> { |
||||
|
return this.restService.request<void, Books.Response>({ |
||||
|
method: 'GET', |
||||
|
url: '/api/app/book' |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicionado o `get`método para obter a lista de livros executando uma solicitação HTTP no terminal relacionado. |
||||
|
|
||||
|
Substitua o `books.actions.ts`conteúdo conforme mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
export class GetBooks { |
||||
|
static readonly type = '[Books] Get'; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Implementar o BooksState |
||||
|
|
||||
|
Abra o `books.state.ts`e altere o arquivo, como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { State, Action, StateContext, Selector } from '@ngxs/store'; |
||||
|
import { GetBooks } from '../actions/books.actions'; |
||||
|
import { Books } from '../models/books'; |
||||
|
import { BooksService } from '../../books/shared/books.service'; |
||||
|
import { tap } from 'rxjs/operators'; |
||||
|
|
||||
|
@State<Books.State>({ |
||||
|
name: 'BooksState', |
||||
|
defaults: { books: {} } as Books.State, |
||||
|
}) |
||||
|
export class BooksState { |
||||
|
@Selector() |
||||
|
static getBooks(state: Books.State) { |
||||
|
return state.books.items || []; |
||||
|
} |
||||
|
|
||||
|
constructor(private booksService: BooksService) {} |
||||
|
|
||||
|
@Action(GetBooks) |
||||
|
get(ctx: StateContext<Books.State>) { |
||||
|
return this.booksService.get().pipe( |
||||
|
tap(booksResponse => { |
||||
|
ctx.patchState({ |
||||
|
books: booksResponse, |
||||
|
}); |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicionada a `GetBooks`ação que usa o `BookService`definido acima para obter os livros e corrigir o estado. |
||||
|
|
||||
|
> O NGXS exige retornar o observável sem assiná-lo, conforme feito nesta amostra (na função get). |
||||
|
|
||||
|
#### BookListComponent |
||||
|
|
||||
|
Modifique o `book-list.component.ts`como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { Store, Select } from '@ngxs/store'; |
||||
|
import { BooksState } from '../../store/states'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { Books } from '../../store/models'; |
||||
|
import { GetBooks } from '../../store/actions'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-book-list', |
||||
|
templateUrl: './book-list.component.html', |
||||
|
styleUrls: ['./book-list.component.scss'], |
||||
|
}) |
||||
|
export class BookListComponent implements OnInit { |
||||
|
@Select(BooksState.getBooks) |
||||
|
books$: Observable<Books.Book[]>; |
||||
|
|
||||
|
booksType = Books.BookType; |
||||
|
|
||||
|
loading = false; |
||||
|
|
||||
|
constructor(private store: Store) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.loading = true; |
||||
|
this.store.dispatch(new GetBooks()).subscribe(() => { |
||||
|
this.loading = false; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> Consulte as [ações de despacho](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) e [selecione](https://ngxs.gitbook.io/ngxs/concepts/select) na documentação do NGXS para obter mais informações sobre esses recursos do NGXS. |
||||
|
|
||||
|
Substitua o `book-list.component.html`conteúdo conforme mostrado abaixo: |
||||
|
|
||||
|
```html |
||||
|
<div id="wrapper" class="card"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row"> |
||||
|
<div class="col col-md-6"> |
||||
|
<h5 class="card-title"> |
||||
|
Books |
||||
|
</h5> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
||||
|
<ng-template pTemplate="header"> |
||||
|
<tr> |
||||
|
<th>Book name</th> |
||||
|
<th>Book type</th> |
||||
|
<th>Publish date</th> |
||||
|
<th>Price</th> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
<ng-template pTemplate="body" let-data> |
||||
|
<tr> |
||||
|
<td>{{ data.name }}</td> |
||||
|
<td>{{ booksType[data.type] }}</td> |
||||
|
<td>{{ data.publishDate | date }}</td> |
||||
|
<td>{{ data.price }}</td> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
</p-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
> Usamos a [tabela PrimeNG](https://www.primefaces.org/primeng/#/table) neste componente. |
||||
|
|
||||
|
A página de livros resultante é mostrada abaixo: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
E esta é a estrutura de pastas e arquivos no final deste tutorial: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
> Este tutorial segue o [Guia de estilo angular](https://angular.io/guide/styleguide#file-tree) . |
||||
|
|
||||
|
### Próxima parte |
||||
|
|
||||
|
Veja a [próxima parte](Part-II.md) deste tutorial. |
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,582 @@ |
|||||
|
## Tutorial Angular - Parte II |
||||
|
|
||||
|
### Sobre este tutorial |
||||
|
|
||||
|
Esta é a segunda parte da série de tutoriais angulares. Veja todas as peças: |
||||
|
|
||||
|
- [Parte I: Crie o projeto e uma página da lista de livros](Part-I.md) |
||||
|
- **Parte II: Criar, atualizar e excluir livros (este tutorial)** |
||||
|
- [Parte III: Testes de Integração](Part-III.md) |
||||
|
|
||||
|
Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb) . |
||||
|
|
||||
|
### Criando um novo livro |
||||
|
|
||||
|
Nesta seção, você aprenderá como criar um novo formulário de diálogo modal para criar um novo livro. |
||||
|
|
||||
|
#### Definição do tipo |
||||
|
|
||||
|
Criar uma interface, com o nome `CreateUpdateBookInput`no `books.ts`como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
export namespace Books { |
||||
|
//... |
||||
|
export interface CreateUpdateBookInput { |
||||
|
name: string; |
||||
|
type: BookType; |
||||
|
publishDate: string; |
||||
|
price: number; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
`CreateUpdateBookInput`interface corresponde ao `CreateUpdateBookDto`no back-end. |
||||
|
|
||||
|
#### Método de Serviço |
||||
|
|
||||
|
Abra o `books.service.ts`e adicione um novo método, nomeado `create`para executar uma solicitação HTTP POST no servidor: |
||||
|
|
||||
|
```js |
||||
|
create(createBookInput: Books.CreateUpdateBookInput): Observable<Books.Book> { |
||||
|
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({ |
||||
|
method: 'POST', |
||||
|
url: '/api/app/book', |
||||
|
body: createBookInput |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `restService.request`A função obtém parâmetros genéricos para os tipos enviados e recebidos do servidor. Este exemplo envia um `CreateUpdateBookInput`objeto e recebe um `Book`objeto (você pode definir o tipo `void`de solicitação ou retorno, se não for usado). |
||||
|
|
||||
|
#### Definições de estado |
||||
|
|
||||
|
Adicione a `CreateUpdateBook`ação ao `books.actions.ts`conforme mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { Books } from '../models'; |
||||
|
|
||||
|
export class CreateUpdateBook { |
||||
|
static readonly type = '[Books] Create Update Book'; |
||||
|
constructor(public payload: Books.CreateUpdateBookInput) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Abra `books.state.ts`e defina o `save`método que ouvirá uma `CreateUpdateBook`ação para criar um livro: |
||||
|
|
||||
|
```js |
||||
|
import { ... , CreateUpdateBook } from '../actions/books.actions'; |
||||
|
import { ... , switchMap } from 'rxjs/operators'; |
||||
|
//... |
||||
|
@Action(CreateUpdateBook) |
||||
|
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { |
||||
|
return this.booksService |
||||
|
.create(action.payload) |
||||
|
.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Quando a `SaveBook`ação é despachada, o método save é executado. Ele chama o `create`método do `BooksService`definido anteriormente. Após a chamada de serviço, `BooksState`despacha a `GetBooks`ação para obter livros novamente do servidor para atualizar a página. |
||||
|
|
||||
|
#### Adicionar um modal ao BookListComponent |
||||
|
|
||||
|
Abra o `book-list.component.html`e adicione o `abp-modal`para mostrar / ocultar o modal para criar um novo livro. |
||||
|
|
||||
|
```html |
||||
|
<abp-modal [(visible)]="isModalOpen"> |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>New Book</h3> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpBody> </ng-template> |
||||
|
|
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose> |
||||
|
Cancel |
||||
|
</button> |
||||
|
</ng-template> |
||||
|
</abp-modal> |
||||
|
``` |
||||
|
|
||||
|
`abp-modal`é um componente pré-construído para mostrar os modais. Embora você possa usar outra abordagem para mostrar um modal, `abp-modal`fornece benefícios adicionais. |
||||
|
|
||||
|
Adicione um botão rotulado `New book`para mostrar o modal: |
||||
|
|
||||
|
```html |
||||
|
<div class="row"> |
||||
|
<div class="col col-md-6"> |
||||
|
<h5 class="card-title"> |
||||
|
Books |
||||
|
</h5> |
||||
|
</div> |
||||
|
<div class="text-right col col-md-6"> |
||||
|
<button id="create-role" class="btn btn-primary" type="button" (click)="createBook()"> |
||||
|
<i class="fa fa-plus mr-1"></i> <span>New book</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
Abra a variável `book-list.component.ts`e adicione `isModalOpen`e `createBook`método para mostrar / ocultar o modal. |
||||
|
|
||||
|
```js |
||||
|
isModalOpen = false; |
||||
|
|
||||
|
//... |
||||
|
|
||||
|
createBook() { |
||||
|
this.isModalOpen = true; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
#### Criar um formulário reativo |
||||
|
|
||||
|
> [Os formulários reativos](https://angular.io/guide/reactive-forms) fornecem uma abordagem orientada a modelo para lidar com entradas de formulário cujos valores mudam ao longo do tempo. |
||||
|
|
||||
|
Adicione uma `form`variável e injete um `FormBuilder`serviço `book-list.component.ts`como mostrado abaixo (lembre-se de adicionar a instrução de importação). |
||||
|
|
||||
|
```js |
||||
|
import { FormGroup, FormBuilder } from '@angular/forms'; |
||||
|
|
||||
|
form: FormGroup; |
||||
|
|
||||
|
constructor( |
||||
|
//... |
||||
|
private fb: FormBuilder |
||||
|
) {} |
||||
|
``` |
||||
|
|
||||
|
> O serviço [FormBuilder](https://angular.io/api/forms/FormBuilder) fornece métodos convenientes para gerar controles. Reduz a quantidade de clichê necessária para criar formulários complexos. |
||||
|
|
||||
|
Adicione o `buildForm`método para criar um formulário de livro. |
||||
|
|
||||
|
```js |
||||
|
buildForm() { |
||||
|
this.form = this.fb.group({ |
||||
|
name: ['', Validators.required], |
||||
|
type: [null, Validators.required], |
||||
|
publishDate: [null, Validators.required], |
||||
|
price: [null, Validators.required], |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- O `group`método de `FormBuilder`( `fb`) cria a `FormGroup`. |
||||
|
- Adicionado `Validators.required`método estático que valida o elemento de formulário relacionado. |
||||
|
|
||||
|
Modifique o `createBook`método como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
createBook() { |
||||
|
this.buildForm(); |
||||
|
this.isModalOpen = true; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Crie os elementos DOM do formulário |
||||
|
|
||||
|
Abra `book-list.component.html`e adicione o formulário no modelo de corpo do modal. |
||||
|
|
||||
|
```html |
||||
|
<ng-template #abpBody> |
||||
|
<form [formGroup]="form"> |
||||
|
<div class="form-group"> |
||||
|
<label for="book-name">Name</label><span> * </span> |
||||
|
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-price">Price</label><span> * </span> |
||||
|
<input type="number" id="book-price" class="form-control" formControlName="price" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-type">Type</label><span> * </span> |
||||
|
<select class="form-control" id="book-type" formControlName="type"> |
||||
|
<option [ngValue]="null">Select a book type</option> |
||||
|
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {{ type }}</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label>Publish date</label><span> * </span> |
||||
|
<input |
||||
|
#datepicker="ngbDatepicker" |
||||
|
class="form-control" |
||||
|
name="datepicker" |
||||
|
formControlName="publishDate" |
||||
|
ngbDatepicker |
||||
|
(click)="datepicker.toggle()" |
||||
|
/> |
||||
|
</div> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
``` |
||||
|
|
||||
|
- Este modelo cria um formulário com os campos Nome, Preço, Tipo e Data de publicação. |
||||
|
|
||||
|
> Usamos o [datepicker do NgBootstrap](https://ng-bootstrap.github.io/#/components/datepicker/overview) neste componente. |
||||
|
|
||||
|
Abra o `book-list.component.ts`e crie uma matriz chamada `bookTypeArr`: |
||||
|
|
||||
|
```js |
||||
|
//... |
||||
|
form: FormGroup; |
||||
|
|
||||
|
bookTypeArr = Object.keys(Books.BookType).filter( |
||||
|
bookType => typeof this.booksType[bookType] === 'number' |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
O `bookTypeArr`contém os campos da `BookType`enumeração. A matriz resultante é mostrada abaixo: |
||||
|
|
||||
|
```js |
||||
|
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] |
||||
|
``` |
||||
|
|
||||
|
Essa matriz foi usada no modelo de formulário anterior (no `ngFor`loop). |
||||
|
|
||||
|
#### Requisitos do Datepicker |
||||
|
|
||||
|
Você precisa importar `NgbDatepickerModule`para o `books.module.ts`: |
||||
|
|
||||
|
```js |
||||
|
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
// ... |
||||
|
NgbDatepickerModule, |
||||
|
], |
||||
|
}) |
||||
|
export class BooksModule {} |
||||
|
``` |
||||
|
|
||||
|
Abra o `book-list.component.ts`e adicione `providers`como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; |
||||
|
|
||||
|
@Component({ |
||||
|
// ... |
||||
|
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], |
||||
|
}) |
||||
|
export class BookListComponent implements OnInit { |
||||
|
// ... |
||||
|
``` |
||||
|
|
||||
|
> O `NgbDateAdapter`valor do Datepicker converte em `Date`tipo. Consulte os [adaptadores datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) para obter mais detalhes. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
#### Salvando o livro |
||||
|
|
||||
|
Abra o `book-list.component.html`e adicione um `abp-button`para salvar o formulário. |
||||
|
|
||||
|
```html |
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose> |
||||
|
Cancel |
||||
|
</button> |
||||
|
<button class="btn btn-primary" (click)="save()"> |
||||
|
<i class="fa fa-check mr-1"></i> |
||||
|
Save |
||||
|
</button> |
||||
|
</ng-template> |
||||
|
``` |
||||
|
|
||||
|
Isso adiciona um botão Salvar à área inferior do modal: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Em seguida, defina um `save`método no `BookListComponent`: |
||||
|
|
||||
|
```js |
||||
|
save() { |
||||
|
if (this.form.invalid) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { |
||||
|
this.isModalOpen = false; |
||||
|
this.form.reset(); |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Atualizando um livro existente |
||||
|
|
||||
|
#### BooksService |
||||
|
|
||||
|
Abra o `books.service.ts`e adicione os métodos `getById`e `update`. |
||||
|
|
||||
|
```js |
||||
|
getById(id: string): Observable<Books.Book> { |
||||
|
return this.restService.request<void, Books.Book>({ |
||||
|
method: 'GET', |
||||
|
url: `/api/app/book/${id}` |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { |
||||
|
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({ |
||||
|
method: 'PUT', |
||||
|
url: `/api/app/book/${id}`, |
||||
|
body: updateBookInput |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Ação CreateUpdateBook |
||||
|
|
||||
|
Abra o parâmetro `books.actins.ts`e adicione `id`à `CreateUpdateBook`ação: |
||||
|
|
||||
|
```js |
||||
|
export class CreateUpdateBook { |
||||
|
static readonly type = '[Books] Create Update Book'; |
||||
|
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Abra `books.state.ts`e modifique o `save`método conforme mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
@Action(CreateUpdateBook) |
||||
|
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { |
||||
|
let request; |
||||
|
|
||||
|
if (action.id) { |
||||
|
request = this.booksService.update(action.payload, action.id); |
||||
|
} else { |
||||
|
request = this.booksService.create(action.payload); |
||||
|
} |
||||
|
|
||||
|
return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### BookListComponent |
||||
|
|
||||
|
Injectar `BooksService`dependência, adicionando-o ao `book-list.component.ts`construtor e adicione uma variável chamada `selectedBook`. |
||||
|
|
||||
|
```js |
||||
|
import { BooksService } from '../shared/books.service'; |
||||
|
//... |
||||
|
selectedBook = {} as Books.Book; |
||||
|
|
||||
|
constructor( |
||||
|
//... |
||||
|
private booksService: BooksService |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
`booksService`é usado para obter o livro de edição para preparar o formulário. Modifique o `buildForm`método para reutilizar o mesmo formulário ao editar um livro. |
||||
|
|
||||
|
```js |
||||
|
buildForm() { |
||||
|
this.form = this.fb.group({ |
||||
|
name: [this.selectedBook.name || '', Validators.required], |
||||
|
type: this.selectedBook.type || null, |
||||
|
publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, |
||||
|
price: this.selectedBook.price || null, |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicione o `editBook`método como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
editBook(id: string) { |
||||
|
this.booksService.getById(id).subscribe(book => { |
||||
|
this.selectedBook = book; |
||||
|
this.buildForm(); |
||||
|
this.isModalOpen = true; |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicionado `editBook`método para obter o livro de edição, criar o formulário e mostrar o modal. |
||||
|
|
||||
|
Agora, adicione a `selectedBook`definição ao `createBook`método para reutilizar o mesmo formulário ao criar um novo livro: |
||||
|
|
||||
|
```js |
||||
|
createBook() { |
||||
|
this.selectedBook = {} as Books.Book; |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Modifique o `save`método para passar o ID do livro selecionado, como mostrado abaixo: |
||||
|
|
||||
|
```js |
||||
|
save() { |
||||
|
if (this.form.invalid) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) |
||||
|
.subscribe(() => { |
||||
|
this.isModalOpen = false; |
||||
|
this.form.reset(); |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Adicione o menu suspenso "Ações" à tabela |
||||
|
|
||||
|
Abra o `book-list.component.html` e adicione modifique o `p-table` como mostrado abaixo: |
||||
|
|
||||
|
```html |
||||
|
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
||||
|
<ng-template pTemplate="header"> |
||||
|
<tr> |
||||
|
<th>Actions</th> |
||||
|
<th>Book name</th> |
||||
|
<th>Book type</th> |
||||
|
<th>Publish date</th> |
||||
|
<th>Price</th> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
<ng-template pTemplate="body" let-data> |
||||
|
<tr> |
||||
|
<td> |
||||
|
<div ngbDropdown class="d-inline-block"> |
||||
|
<button |
||||
|
class="btn btn-primary btn-sm dropdown-toggle" |
||||
|
data-toggle="dropdown" |
||||
|
aria-haspopup="true" |
||||
|
ngbDropdownToggle |
||||
|
> |
||||
|
<i class="fa fa-cog mr-1"></i>Actions |
||||
|
</button> |
||||
|
<div ngbDropdownMenu> |
||||
|
<button ngbDropdownItem (click)="editBook(data.id)">Edit</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</td> |
||||
|
<td>{{ data.name }}</td> |
||||
|
<td>{{ booksType[data.type] }}</td> |
||||
|
<td>{{ data.publishDate | date }}</td> |
||||
|
<td>{{ data.price }}</td> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
</p-table> |
||||
|
``` |
||||
|
|
||||
|
- Adicionado um `th`para a coluna "Ações". |
||||
|
- Adicionado `button`com `ngbDropdownToggle`para abrir ações quando clicamos no botão. |
||||
|
|
||||
|
> Nós costumávamos usar o [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) no menu suspenso de ações. |
||||
|
|
||||
|
A interface do usuário final é semelhante a: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Atualize o cabeçalho modal para alterar o título com base na operação atual: |
||||
|
|
||||
|
```html |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>{{ selectedBook.id ? 'Edit' : 'New Book' }}</h3> |
||||
|
</ng-template> |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Exclusão de um livro existente |
||||
|
|
||||
|
#### BooksService |
||||
|
|
||||
|
Abra `books.service.ts`e inclua um `delete`método para excluir um livro com o `id`, executando uma solicitação HTTP no nó de extremidade relacionado: |
||||
|
|
||||
|
```js |
||||
|
delete(id: string): Observable<void> { |
||||
|
return this.restService.request<void, void>({ |
||||
|
method: 'DELETE', |
||||
|
url: `/api/app/book/${id}` |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Ação DeleteBook |
||||
|
|
||||
|
Adicione uma ação chamada `DeleteBook`para `books.actions.ts`: |
||||
|
|
||||
|
```js |
||||
|
export class DeleteBook { |
||||
|
static readonly type = '[Books] Delete'; |
||||
|
constructor(public id: string) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Abra o `books.state.ts`e adicione o `delete`método que ouvirá a `DeleteBook`ação para excluir um livro: |
||||
|
|
||||
|
```js |
||||
|
import { ... , DeleteBook } from '../actions/books.actions'; |
||||
|
//... |
||||
|
@Action(DeleteBook) |
||||
|
delete(ctx: StateContext<Books.State>, action: DeleteBook) { |
||||
|
return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Adicionado `DeleteBook`à lista de importação. |
||||
|
- Usa `bookService`para excluir o livro. |
||||
|
|
||||
|
\#### Adicionar um botão Excluir |
||||
|
|
||||
|
Abra `book-list.component.html`e modifique `ngbDropdownMenu`para adicionar o botão excluir, como mostrado abaixo: |
||||
|
|
||||
|
```html |
||||
|
<div ngbDropdownMenu> |
||||
|
... |
||||
|
<button ngbDropdownItem (click)="delete(data.id, data.name)"> |
||||
|
Delete |
||||
|
</button> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
A interface do usuário suspensa de ações finais é semelhante a abaixo: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
\#### Caixa de diálogo Excluir confirmação |
||||
|
|
||||
|
Abra `book-list.component.ts`e injete o `ConfirmationService`. |
||||
|
|
||||
|
```js |
||||
|
import { ConfirmationService } from '@abp/ng.theme.shared'; |
||||
|
//... |
||||
|
constructor( |
||||
|
//... |
||||
|
private confirmationService: ConfirmationService |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
> `ConfirmationService` é um serviço simples fornecido pela estrutura ABP que usa internamente o PrimeNG. |
||||
|
|
||||
|
Adicione um método de exclusão ao `BookListComponent`: |
||||
|
|
||||
|
```js |
||||
|
import { ... , DeleteBook } from '../../store/actions'; |
||||
|
import { ... , Toaster } from '@abp/ng.theme.shared'; |
||||
|
//... |
||||
|
delete(id: string, name: string) { |
||||
|
this.confirmationService |
||||
|
.error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') |
||||
|
.subscribe(status => { |
||||
|
if (status === Toaster.Status.confirm) { |
||||
|
this.store.dispatch(new DeleteBook(id)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
O `delete`método mostra um pop-up de confirmação e assina a resposta do usuário. `DeleteBook`ação despachada somente se o usuário clicar no `Yes`botão O pop-up de confirmação é exibido abaixo: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Próxima parte |
||||
|
|
||||
|
Veja a [próxima parte](Part-III.md) deste tutorial. |
||||
|
|
||||
@ -0,0 +1,181 @@ |
|||||
|
## Tutorial do ASP.NET Core MVC - Parte III |
||||
|
|
||||
|
### Sobre este tutorial |
||||
|
|
||||
|
Esta é a terceira parte da série de tutoriais Angular. Veja todas as peças: |
||||
|
|
||||
|
- [Parte I: Crie o projeto e uma página da lista de livros](Part-I.md) |
||||
|
- [Parte II: Criar, atualizar e excluir livros](Part-II.md) |
||||
|
- **Parte III: Testes de Integração (este tutorial)** |
||||
|
|
||||
|
Esta parte abrange os testes do **lado** do **servidor** . Você pode acessar o **código fonte** do aplicativo no [repositório GitHub](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb) . |
||||
|
|
||||
|
### Testar projetos na solução |
||||
|
|
||||
|
Existem vários projetos de teste na solução: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Cada projeto é usado para testar o projeto de aplicativo relacionado. Os projetos de teste usam as seguintes bibliotecas para teste: |
||||
|
|
||||
|
- [xunit](https://xunit.github.io/) como a principal estrutura de teste. |
||||
|
- [Shouldly](http://shouldly.readthedocs.io/en/latest/) como uma biblioteca de asserções. |
||||
|
- [NSubstitute](http://nsubstitute.github.io/) como uma biblioteca de zombaria. |
||||
|
|
||||
|
### Adicionando dados de teste |
||||
|
|
||||
|
O modelo de inicialização contém a `BookStoreTestDataSeedContributor`classe no `Acme.BookStore.TestBase`projeto que cria alguns dados para executar os testes. |
||||
|
|
||||
|
Mude a `BookStoreTestDataSeedContributor`classe como mostrado abaixo: |
||||
|
|
||||
|
```csharp |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.Guids; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookStoreTestDataSeedContributor |
||||
|
: IDataSeedContributor, ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
private readonly IGuidGenerator _guidGenerator; |
||||
|
|
||||
|
public BookStoreTestDataSeedContributor( |
||||
|
IRepository<Book, Guid> bookRepository, |
||||
|
IGuidGenerator guidGenerator) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
_guidGenerator = guidGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task SeedAsync(DataSeedContext context) |
||||
|
{ |
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Id = _guidGenerator.Create(), |
||||
|
Name = "Test book 1", |
||||
|
Type = BookType.Fantastic, |
||||
|
PublishDate = new DateTime(2015, 05, 24), |
||||
|
Price = 21 |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book |
||||
|
{ |
||||
|
Id = _guidGenerator.Create(), |
||||
|
Name = "Test book 2", |
||||
|
Type = BookType.Science, |
||||
|
PublishDate = new DateTime(2014, 02, 11), |
||||
|
Price = 15 |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Injetado `IRepository<Book, Guid>`e usado no `SeedAsync`para criar duas entidades de livro como dados de teste. |
||||
|
- `IGuidGenerator`Serviço usado para criar GUIDs. Embora `Guid.NewGuid()`funcionasse perfeitamente para testes, `IGuidGenerator`possui recursos adicionais especialmente importantes ao usar bancos de dados reais (consulte o documento de geração do [Guid](../../Guid-Generation.md) para obter mais informações). |
||||
|
|
||||
|
### Testando o BookAppService |
||||
|
|
||||
|
Crie uma classe de teste denominada `BookAppService_Tests`no `Acme.BookStore.Application.Tests`projeto: |
||||
|
|
||||
|
```csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookAppService_Tests : BookStoreApplicationTestBase |
||||
|
{ |
||||
|
private readonly IBookAppService _bookAppService; |
||||
|
|
||||
|
public BookAppService_Tests() |
||||
|
{ |
||||
|
_bookAppService = GetRequiredService<IBookAppService>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_List_Of_Books() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.GetListAsync( |
||||
|
new PagedAndSortedResultRequestDto() |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.TotalCount.ShouldBeGreaterThan(0); |
||||
|
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `Should_Get_List_Of_Books`O teste simplesmente usa o `BookAppService.GetListAsync`método para obter e verificar a lista de usuários. |
||||
|
|
||||
|
Adicione um novo teste que crie um novo livro válido: |
||||
|
|
||||
|
```csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Create_A_Valid_Book() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "New test book 42", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.Id.ShouldNotBe(Guid.Empty); |
||||
|
result.Name.ShouldBe("New test book 42"); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Adicione um novo teste que tente criar um livro inválido e falhe: |
||||
|
|
||||
|
```csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Create_A_Book_Without_Name() |
||||
|
{ |
||||
|
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
||||
|
{ |
||||
|
await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
exception.ValidationErrors |
||||
|
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- Como o `Name`está vazio, o ABP lança um `AbpValidationException`. |
||||
|
|
||||
|
Abra a **janela Test Explorer** (use o menu Test -> Windows -> Test Explorer, se não estiver visível) e **execute Todos os** testes: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Parabéns, ícones verdes mostram que os testes foram aprovados com sucesso! |
||||
|
|
||||
|
|
||||
|
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 18 KiB |