Angular 5 – это фреймворк позволяющий быстро и удобно разрабатывать одностраничные веб приложения. В своей основе он использует язык программирования TypeScript. Давайте рассмотрим процесс создания SPA-приложения (Single Page Application) с помощью данного фреймворка от идеи до публикации.
Продолжим создание нашего приложения. Первая часть доступна по ссылке Создание Angular 5 приложения. Для того, чтобы сделать более функциональное приложение нам необходимо создать классы данных, удалить жестко зафиксированные данные из кода. Этим мы сейчас и займемся. Для начала создадим класс модели данных card.type.ts в папке shared. Здесь мы указываем все необходимые переменные.
import { CardType } from './card-type.type'; import { CardCity } from './card-city.type'; export class Card { id: number; date: Date; title: string; phone: string; city: CardCity; type: CardType; price: number; name: string; tags: string[]; site: string; text: string; longText: string; email: string; }
Теперь необходимо создать два недостающих класса CardType и CardCity
export class CardCity { id: number; name: string; }
export class CardType { id: number; name: string; }
Теперь применим класс объявления. Перейдем в файл home.component.ts и добавим свойства массив объявлений, не забыв импортировать класс объявления.
import { Component } from '@angular/core'; import { Card } from '../shared/card.type'; import { CardService } from '../shared/card.service'; @Component({ selector: 'home', templateUrl: './home.component.html' }) export class HomeComponent { cards: Card[]; }
Изменим файл представления. Добавим вывод всех элементов массива с помощью цикла *ngFor. А в случае, если в массиве не будет элементов, будем выводить информационное сообщение.
<div class="alert alert-info text-center" role="alert" *ngIf="!cards"> Загрузка объявлений. </div> <div class="alert alert-info text-center" role="alert" *ngIf="cards?.length == 0"> Объявлений нет. </div> <card-summary-component *ngFor="let card of cards"></card-summary-component>
Теперь в конструкторе создадим один элемент массива.
export class HomeComponent { cards: Card[]; constructor() { this.cards = [ { id: 1, title: "Заголовок из кода", date: new Date(), email: "adm.shwan@gmail.com", longText: "Очень подробное описание объявления", name: "Вадим", phone: "+7 (920) 737-1024", price: 0, site: "https://shwanoff.ru", tags: "объявление, тест", text: "Описание объявления", type: { id: 1, name: "Категория 1" }, city: { id: 1, name: "Курск" } } ]; } }
После этого на странице у нас отобразиться одно объявление, но все еще оно будет содержать неправильные данные. Для того, чтобы передать данные для начала изменим вывод объявлений так, чтобы объект передавался в качестве параметра в компонент card-summary
<card-summary-component *ngFor="let card of cards" [card]="card"></card-summary-component>
Затем идем в сам компонент объявления и импортируем библиотеку Input. Добавляем входную переменную card. И наконец меняем правило навигации, чтобы выполнялся переход по идентификатору из переменной card.
import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { Card } from '../../shared/card.type'; @Component({ selector: 'card-summary-component', templateUrl: './card-summary.component.html' }) export class CardSummaryComponent { @Input() card: Card; constructor(private router: Router) { } goToDetail() { this.router.navigate(["card", this.card.id]); } }
И наконец меняем отображение данных на карте в файле card-summary.component.html
<div class="card" (click)="goToDetail()"> <div class="card-body"> <div class="card-body"> <h4>{{card.title}}</h4> <h6 class="card-subtitle text-muted">{{card.date}}</h6> <p class="card-text">{{card.text}} {{card.id}}</p> </div> </div> </div>
Теперь аналогичным образом изменим компонент подробностей объявления.
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CurrencyPipe } from '@angular/common'; import { Card } from '../../shared/card.type' @Component({ selector: 'card-detail-component', templateUrl: './card-detail.component.html' }) export class CardDetailComponent implements OnInit { card: Card; constructor(private route: ActivatedRoute) { this.card = new Card(); } ngOnInit() { this.route.params.subscribe(params => { this.card.id = params["id"]; this.card.title = "Заголовок из кода 1"; this.card.date = new Date(); this.card.email = "adm.shwan@gmail.com"; this.card.longText = "Очень подробное описание объявления"; this.card.name = "Вадим"; this.card.phone = "+7 (920) 737-1024"; this.card.price = 0; this.card.site = "https://shwanoff.ru"; this.card.text = "Описание объявления из кода 1"; this.card.type = { id: 1, name: "Категория 1" }; this.card.city = { id: 1, name: "Курск" }; this.card.tags = ["Объявление", "Тест", "Тег"]; console.log(this.card); }); } }
Обратите внимание, что здесь мы делаем несколько интересных операций. Во-первых, мы используем ActivatedRoute для получения идентификатора из параметра URL.
this.route.params.subscribe(params => { this.card.id = params["id"];
Во-вторых, мы наследуем событие OnInit, которое происходит при загрузке компонента.
Ну и естественно нам необходимо поправить представление в соответствии с моделью.
<div class="card"> <div class="card-header"> <h3 class="card-title">{{card.title}}</h3> <p class="card-subtitle card-title">{{card.date}}</p> <p class="card-title float-left">{{card.city?.name}}</p> <p class="card-title float-right">{{card.type?.name}}</p> </div> <div class="card-body"> <div class="card-text"> <h6>{{card.text}}</h6> <p>{{card.longtext}}</p> <p class="text-right">{{card.price | currency:'RUB':'symbol-narrow'}}</p> <div class="d-block"> <p> <strong>{{card.name}}</strong> </p> <p class="float-right"> {{card.phone}} </p> </div> <p> <a href="{{card.site}}">{{card.site}}</a> </p> <a href="#" class="badge badge-secondary" *ngFor="let tag of card.tags">{{tag}}</a> </div> </div> </div>
Теперь изменим компонент добавления объявления аналогичным образом используя класс модели.
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; import { NgModel } from '@angular/forms'; import { Card } from '../../shared/card.type'; @Component({ selector: 'card-add-component', templateUrl: './card-add.component.html' }) export class CardAddComponent { card: Card; constructor() { this.card = new Card(); } add() { console.log(this.card); } }
В представлении нам понадобится везде изменить обращение не напрямую к переменной компонента, а к свойству модели. Например [(ngModel)]=»title» заменим на [(ngModel)]=»card.title», и по аналогии для всех остальных полей ввода.
Теперь давайте приступим к настройке взаимодействия нашего фронтэнда с бекендом. Нам необходимо настроить обращение к базе данных для получения и отправки данных. Для начала создадим файл сервиса объявления.
import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; import 'rxjs/Rx'; import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { Card } from '../../components/shared/card.type'; @Injectable() export class AppModule { constructor(private http: Http) { } getAllCards() { return this.http.get("api/card/getall") .map(response => response.json() as Card[]) .toPromise(); } getCard(id: number) { return this.http.get("api/card/get/${id}") .map(response => response.json() as Card) .toPromise(); } addCard(card: Card) { return this.http.post("api/card/add", card); } }
Здесь мы определили три метода, которые будем вызвать для обращения к базе данных. Теперь регистрируем данный сервис в app.shared.module.ts. Импортируем компонент
import { CardService } from './components/shared/card.service';
И добавляем его в список провайдеров
@NgModule({ declarations: [ AppComponent, HomeComponent, HeaderComponent, DescriptionComponent, TypeComponent, SearchComponent, FooterComponent, CardSummaryComponent, CardDetailComponent, CardAddComponent ], imports: [ CommonModule, HttpModule, FormsModule, RouterModule.forRoot([ { path: '', component: HomeComponent }, { path: 'add', component: CardAddComponent }, { path: 'card/:id', component: CardDetailComponent }, { path: '**', redirectTo: '' } ]) ], providers: [CardService] })
Теперь добавим вызов нашего сервиса в компоненты, на забыв его импортировать в них.
home.component.ts
import { Component, OnInit } from '@angular/core'; import { Card } from '../shared/card.type'; import { CardService } from '../shared/card.service'; @Component({ selector: 'home', templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { cards: Card[]; constructor(private cardService: CardService) { } ngOnInit() { this.cardService.getAllCards() .then(cards => this.cards = cards); } }
Здесь при инициализации мы вызываем метод получающий все объявления и сохраняем их в локальную переменную.
card-detail.component.ts
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CurrencyPipe } from '@angular/common'; import { Card } from '../../shared/card.type' import { CardService } from '../../shared/card.service'; @Component({ selector: 'card-detail-component', templateUrl: './card-detail.component.html' }) export class CardDetailComponent implements OnInit { card: Card; constructor(private route: ActivatedRoute, private cardService: CardService) { this.card = new Card(); } ngOnInit() { this.route.params.subscribe(params => { let id = +params["id"]; this.cardService.getCard(id) .then(card => { this.card = card; console.log(this.card); }); }); } }
Здесь мы получаем идентификатор в локальную переменную, а затем вызываем метод получения объявления по идентификатору и сохраняем результат.
card-add.component.ts
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { NgForm } from '@angular/forms'; import { NgModel } from '@angular/forms'; import { Card } from '../../shared/card.type'; import { CardService } from '../../shared/card.service'; @Component({ selector: 'card-add-component', templateUrl: './card-add.component.html' }) export class CardAddComponent { card: Card; constructor(private cardService: CardService, private router: Router) { this.card = new Card(); } add() { this.cardService.addCard(this.card) .then(card => { this.card = card; this.router.navigate(["card", card.id]); }); } }
И наконец здесь мы вызываем добавление нового объявления в базу данных передавая полученные с формы данные, а затем перезаписываем результат в локальную переменную (для получения идентификатора)
Контроллер C# и работа с БД
Приступим к созданию бекенда. Создадим в корне проекта папку Models, в которую добавим класс Card.cs, в котором перечислим все поля рекламного объявления. Будем сразу проектировать модель с учетом использование ORM фреймворка EntityFramework 6.
using System; using System.Runtime.Serialization; namespace Adsmini.Models { /// <summary> /// Объявление. /// </summary> [DataContract(Name = "card")] public class Card { /// <summary> /// Уникальный идентификатор объявления. /// </summary> [DataMember(Name = "id")] public int Id { get; set; } /// <summary> /// Дата и время создания объявления. /// </summary> [DataMember(Name = "date")] public DateTime Date { get; set; } /// <summary> /// Заголовок. /// </summary> [DataMember(Name = "title")] public string Title { get; set; } /// <summary> /// Краткое описание. /// </summary> [DataMember(Name = "text")] public string Text { get; set; } /// <summary> /// Подробное описание. /// </summary> [DataMember(Name = "longtext")] public string LognText { get; set; } /// <summary> /// Идентификатор категории. /// </summary> [DataMember(Name = "type")] public int TypeId { get; set; } /// <summary> /// Контактный телефон. /// </summary> [DataMember(Name = "phone")] public string Phone { get; set; } /// <summary> /// Имя. /// </summary> [DataMember(Name = "name")] public string Name { get; set; } /// <summary> /// Идентификатор города. /// </summary> [DataMember(Name = "city")] public int? CityId { get; set; } /// <summary> /// Стоимость. /// </summary> [DataMember(Name = "price")] public decimal Price { get; set; } /// <summary> /// Ключевые слова. /// </summary> [DataMember(Name = "tags")] public string Tags { get; set; } /// <summary> /// Сайт. /// </summary> [DataMember(Name = "site")] public string Site { get; set; } /// <summary> /// Электронная почта. /// </summary> [DataMember(Name = "email")] public string Email { get; set; } /// <summary> /// Категория объявления. /// </summary> public Type Type { get; set; } /// <summary> /// Город. /// </summary> public virtual City City { get; set; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Заголовок. </returns> public override string ToString() { return Title; } } }
Обратите внимание, что для успешной десериализации данных нам необходимо указывать с помощью атрибута [DataMember(Name = «id»)] имена свойств, как в typescript классе.
Также добавим вспомогательные классы справочники City и Type.
City.cs
using System.Collections.Generic; using System.Runtime.Serialization; namespace Adsmini.Models { /// <summary> /// Город. /// </summary> [DataContract(Name = "city")] public class City { /// <summary> /// Уникальный идентификатор города. /// </summary> [DataMember(Name = "id")] public int Id { get; set; } /// <summary> /// Название города. /// </summary> [DataMember(Name = "name")] public string Title { get; set; } /// <summary> /// Объявления. /// </summary> public virtual ICollection<Card> Cards { get; set; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Название города. </returns> public override string ToString() { return Title; } } }
Type.cs
using System.Collections.Generic; using System.Runtime.Serialization; namespace Adsmini.Models { /// <summary> /// Категория. /// </summary> [DataContract(Name = "type")] public class Type { /// <summary> /// Уникальный идентификатор объявления. /// </summary> [DataMember(Name = "id")] public int Id { get; set; } /// <summary> /// Название. /// </summary> [DataMember(Name = "name")] public string Title { get; set; } /// <summary> /// Объявления. /// </summary> public virtual ICollection<Card> Cards { get; set; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Название категории. </returns> public override string ToString() { return Title; } } }
Теперь подключаем библиотеку EntityFramework 6 из nuget. Заходим в панель управления пакетами решения nuget.

Находим пакет EntityFramework, отмечаем проект, в который нужно установить данный пакет и нажимаем на кнопку установки. После этого подтверждаем установку и ожидаем ее завершения.
Обратите внимание, что стандартная библиотека EntityFramework для .net framework не будет работать в .net core приложении. Нам необходимо установить три библиотеки:
- EntityFrameworkCore
- EntityFrameworkCore.SqlServer
- EntityFrameworkCore.Tools

Теперь приступим к созданию класса контекста, позволяющего настроить подключение к БД и в дальнейшем взаимодействовать с ней. Для этого в папке моделей создадим класс AdsminiContext. Он должен наследовать класс DbContext из пространства имен Microsoft.EntityFrameworkCore.
using Microsoft.EntityFrameworkCore; namespace Adsmini.Models { /// <summary> /// Контекст подключения к базе данных. /// </summary> public class AdsminiContext : DbContext { /// <summary> /// Создать новый экземпляр контекста данных. /// </summary> public AdsminiContext() { // Гарантируем, что база данных существует. Database.EnsureCreated(); } /// <summary> /// Конфигуратор подключения. /// </summary> /// <param name="optionsBuilder"> Свойства конфигуратора. </param> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Указываем строку подключения к базе данных. optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=adsmini;Trusted_Connection=True;"); } /// <summary> /// Объявления. /// </summary> public DbSet<Card> Cards { get; set; } /// <summary> /// Категории. /// </summary> public DbSet<Type> Types { get; set; } /// <summary> /// Города. /// </summary> public DbSet<City> Cities { get; set; } } }
Теперь перейдем к созданию контроллера. За основу мы можем взять автоматически сгенерированный контроллер EF. Для его создания зайдем в папку контроллеров и в контекстном меню выберем добавить контроллер.

Выбираем использование MVC сонтроллера EntityFramework

После этого выбираем класс модели, созданный ранее контекст данных, создание представлений можно отключить.

Из сгенерированного контроллера удалим не нужные нам методы, оставим только конструктор, index, details и два Create.
Для первоначальной проверки работы запросов можно не использовать подключение к базе данных, а просто добавить список в контроллер.
using Adsmini.Models; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; namespace Adsmini.Controllers { [Route("api/card")] public class CardController : Controller { private static List<Card> _cards = new List<Card> { new Card{ Id = 1, Title = "Заголовок из БД", Date = DateTime.Now, Text = "Не очень подробное описание"} }; [HttpGet("getall")] public IActionResult Index() { return new ObjectResult(_cards); } [HttpGet("get/{id}")] public IActionResult Details(int id) { var card = _cards.SingleOrDefault(m => m.Id == id); if (card == null) { return NotFound(); } return new ObjectResult(card); } [HttpPost("add")] public IActionResult Add([FromBody]Card card) { var rnd = new Random(); card.Id = rnd.Next(2, 10000000); card.Date = DateTime.Now.ToUniversalTime(); _cards.Add(card); return new ObjectResult(card); } } }
Ну и наконец реализуем контроллер с использованием базы данных.
using Adsmini.Models; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; namespace Adsmini.Controllers { [Route("api/card")] public class CardController : Controller { private readonly AdsminiContext _context; public CardController() { _context = new AdsminiContext(); } [HttpGet("getall")] public IActionResult Index() { var cards = _context.Cards.ToList().Take(20); return new ObjectResult(cards); } [HttpGet("get/{id}")] public IActionResult Details(int id) { var card = _context.Cards.Find(id); if (card == null) { return NotFound(); } return new ObjectResult(card); } [HttpPost("add")] public IActionResult Add([FromBody]Card card) { if (card == null) { return NotFound(); } card.Date = DateTime.Now.ToUniversalTime(); _context.Cards.Add(card); _context.SaveChanges(); return new ObjectResult(card); } } }
Заключение Angular 5
На данном этапе мы реализовали взаимодействие с базой данных.. Исходный код доступен в репозитории github. В следующей статье мы реализуем весь оставшийся функционал системы и опубликуем приложение в сети. Завершающая цикл статья Создание Angular 5 приложения. Часть 3.