Angular – это фреймворк позволяющий быстро и удобно разрабатывать одностраничные веб приложения. В своей основе он использует язык программирования TypeScript. Давайте рассмотрим процесс создания SPA-приложения (Single Page Application) с помощью данного фреймворка от идеи до публикации.
Предыдущие статьи:
Продолжаем добавлять функциональность нашему приложению. Создаем контроллеры для работы со справочниками категорий объявлений и городов. Данные контроллеры должны позволять получить все элементы справочника или отдельный элемент по его идентификатору.
CityController
using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using Adsmini.Models; namespace Adsmini.Controllers { [Produces("application/json")] [Route("api/city")] public class CityController : Controller { private readonly AdsminiContext _context; public CityController() { _context = new AdsminiContext(); } [HttpGet("getall")] public List<City> GetCities() { var cities = _context.Cities.ToList(); return cities; } [HttpGet("get/{id}")] public IActionResult GetCity([FromRoute]int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var city = _context.Cities.Find(id); if (city == null) { return NotFound(); } return new ObjectResult(city); } } }
TypesController
using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using Adsmini.Models; namespace Adsmini.Controllers { [Produces("application/json")] [Route("api/type")] public class TypesController : Controller { private readonly AdsminiContext _context; public TypesController() { _context = new AdsminiContext(); } [HttpGet("getall")] public List<Models.Type> GetTypes() { var types = _context.Types.ToList(); return types; } [HttpGet("get/{id}")] public IActionResult GetType([FromRoute]int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var type = _context.Types.Find(id); if (type == null) { return NotFound(); } return new ObjectResult(type); } } }
Теперь перейдем в файл card.service.ts и добавим методы вызова нашего серверного кода на языке C# из angular. Для этого для начала импортируем классы города и категории
import { CardType } from '../../components/shared/card-type.type'; import { CardCity } from '../../components/shared/card-city.type';
И добавим соответствующие методы для получения данных
getAllTypes() { return this.http.get("api/type/getall/") .map(response => response.json() as CardType[]) .toPromise(); } getType(id: number) { return this.http.get(`api/type/get/${id}`) .map(response => response.json() as CardType) .toPromise(); } getAllCities() { return this.http.get("api/city/getall/") .map(response => response.json() as CardCity[]) .toPromise(); } getCity(id: number) { return this.http.get(`api/city/get/${id}`) .map(response => response.json() as CardCity) .toPromise(); }
Теперь начинаем вносить исправления в компоненты. Добавим заполнение выпадающих списков на форму добавления объявления.
card-add.component.ts
import { Component, OnInit } 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'; import { CardType } from '../../shared/card-type.type'; import { CardCity } from '../../shared/card-city.type'; @Component({ selector: 'card-add-component', templateUrl: './card-add.component.html' }) export class CardAddComponent implements OnInit { card: Card; types: CardType[]; cities: CardCity[]; constructor(private cardService: CardService, private router: Router) { this.card = new Card(this.cardService); } ngOnInit(): void { this.cardService.getAllTypes() .then(types => { this.types = types; }); this.cardService.getAllCities() .then(cities => { this.cities = cities; }); } add() { this.cardService.addCard(this.card) .then(card => { this.card = card; this.router.navigate(["card", card.id]); }); } }
Здесь при инициализации класса мы обращаемся к базе данных, чтобы получить необходимые справочники категорий и городов, и сохраняем их в соответствующих переменных. Теперь изменим представление компонента, чтобы вывести данные на форму добавления.
<div class="alert alert-info" role="alert" [hidden]="addForm.form.valid"> Заполните все обязательные поля. </div> <div class="card"> <div class="card-header"> <h3 class="card-title">Добавить объявление</h3> </div> <div class="card-body"> <div class="card-text"> <form (ngSubmit)="add()" #addForm="ngForm"> <div class="form-group"> <label for="title"><strong>Заголовок*</strong></label> <input type="text" class="form-control" id="title" name="title" required [(ngModel)]="card.title" placeholder="Введите короткий заголовок объявления (50 символов)..."> </div> <div class="form-group"> <label for="text"><strong>Краткое описание*</strong></label> <input type="text" class="form-control" id="text" name="text" required [(ngModel)]="card.text" placeholder="Введите короткое описание объявления (140 символов)..."> </div> <div class="form-group"> <label for="type"><strong>Категория*</strong></label> <select class="form-control" id="type" name="type" required [(ngModel)]="card.typeid"> <option *ngFor="let type of types" [value]="type.id">{{type.name}}</option> </select> </div> <div class="form-group"> <label for="longText">Подробное описание</label> <input type="text" class="form-control" id="longText" name="longText" [(ngModel)]="card.longText" placeholder="Введите подробное описание..."> </div> <div class="form-group"> <label for="price">Цена</label> <input type="number" class="form-control" id="price" name="price" [(ngModel)]="card.price" placeholder="0.00"> </div> <div class="form-group"> <label for="name">Имя</label> <input type="text" class="form-control" id="name" name="name" [(ngModel)]="card.name" placeholder="Введите Ваше имя..."> </div> <div class="form-group"> <label for="phone">Контактный телефон</label> <input type="tel" class="form-control" id="phone" name="phone" [(ngModel)]="card.phone" placeholder="+7 (999) 999-9999"> </div> <div class="form-group"> <label for="email">Электронная почта</label> <input type="email" class="form-control" id="email" name="email" [(ngModel)]="card.email" placeholder="mail@domen.com"> </div> <div class="form-group"> <label for="site">Сайт</label> <input type="url" class="form-control" id="site" name="site" [(ngModel)]="card.site" placeholder="https://shwanoff.ru/"> </div> <div class="form-group"> <label for="city">Город</label> <select class="form-control" id="city" name="city" [(ngModel)]="card.cityid"> <option *ngFor="let city of cities" [value]="city.id">{{city.name}}</option> </select> </div> <div class="form-group"> <label for="tags">Теги</label> <input type="text" class="form-control" id="tags" name="tags" [(ngModel)]="card.tags" placeholder="Введите теги через запятую..."> </div> <button type="submit" class="btn btn-primary float-right" [disabled]="!addForm.form.valid"> Добавить </button> </form> </div> </div> </div>
Теперь добавим реальный функционал компоненту всех категорий type.component. Для этого для начала нам нужно будет изменить контроллер C#, так чтобы EntityFramework динамически подгружал связанные записи.
CardController.cs
using Adsmini.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System; using System.Linq; namespace Adsmini.Controllers { [Produces("application/json")] [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 .Include(card => card.Type) .Include(card => card.City) .Take(200) .ToList(); return new ObjectResult(cards); } [HttpGet("get/{id}")] public IActionResult Details(int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var card = _context.Cards .Include(c => c.Type) .Include(c => c.City) .Where(c => c.Id == id) .SingleOrDefault(); if (card == null) { return NotFound(); } return new ObjectResult(card); } [HttpPost("add")] public IActionResult Add([FromBody]Card card) { if (card == null) { return NotFound(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } card.Date = DateTime.UtcNow; _context.Cards.Add(card); _context.SaveChanges(); return new ObjectResult(card); } } }
Здесь мы используем команды Include, чтобы указать свойства которые будут динамически связаны. Также изменим модель категории
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> [DataMember(Name = "count")] public int Count { get { return Cards?.Count ?? 0; } } /// <summary> /// Объявления. /// </summary> public virtual ICollection<Card> Cards { get; set; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Название категории. </returns> public override string ToString() { return Title; } } }
Мы добавили вычисляемое свойство, в котором будут содержаться количество всех объявлений данной категории. Также необходимо изменить контроллер категории, также для динамической связи.
TypesController.cs
using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using Adsmini.Models; using Microsoft.EntityFrameworkCore; namespace Adsmini.Controllers { [Produces("application/json")] [Route("api/type")] public class TypesController : Controller { private readonly AdsminiContext _context; public TypesController() { _context = new AdsminiContext(); } [HttpGet("getall")] public List<Models.Type> GetTypes() { var types = _context.Types .Include(type => type.Cards) .ToList() .OrderByDescending(type => type.Count) .ToList(); return types; } [HttpGet("get/{id}")] public IActionResult GetType([FromRoute]int id) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var type = _context.Types.Find(id); if (type == null) { return NotFound(); } return new ObjectResult(type); } } }
Ну и наконец изменим сам компонент.
type.component.ts
import { Component, Input, OnInit } from '@angular/core'; import { CardType } from '../card-type.type'; import { CardService } from '../card.service'; @Component({ selector: 'type-component', templateUrl: './type.component.html' }) export class TypeComponent implements OnInit { @Input() title: string; items: CardType[]; constructor(private cardService: CardService) { } ngOnInit(): void { this.cardService.getAllTypes() .then(types => { this.items = types; console.log(this.items); }); } }
type.component.html
<div id="types"> <div class="card"> <div class="card-header"> <a href="#">{{title}}</a> </div> <ul class="list-group list-group-flush"> <li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" *ngFor="let item of items"> {{item.name}} <span class="badge badge-primary badge-pill">{{item.count}}</span> </li> </ul> </div> </div>
Теперь реализуем фильтрацию объявлений по выбранной категории. Добавляем метод в контроллере объявлений, позволяющий получить все объявления выбранной категории.
[HttpGet("getbytype/{id}")] public IActionResult Filtered(int id) { var cards = _context.Cards .Where(card => card.TypeId == id) .Include(card => card.Type) .Include(card => card.City) .Take(200) .ToList(); return new ObjectResult(cards); }
Добавим метод для обращения к базе данных в файле card.service
getCardsByType(id: number) { return this.http.get(`api/card/getbytype/${id}`) .map(response => response.json() as Card[]) .toPromise(); }
Добавим новое правило навигации в файле app.shared.module.ts
{ path: 'type/:id', component: HomeComponent },
Добавим в файле type.component.ts метод для перехода на фильтрованные объявления
getCards(id: number): void { this.router.navigate(["type", id]); }
Ну и соответственно изменим представление компонента, чтобы при нажатии на элемент вызывался данный метод
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" *ngFor="let item of items" (click)="getCards(item?.id)">
Больше всего изменений придется сделать в файле home.component.ts. Здесь мы добавляем проверку. Если был передан идентификатор, то выводим фильтрованные объявления. Иначе выводим все.
ngOnInit() { this.route.params.subscribe(params => { let id = +params["id"]; if (id) { this.cardService.getCardsByType(id) .then(cards => { this.cards = cards; }); } else { this.cardService.getAllCards() .then(cards => this.cards = cards); } }); }
Теперь давайте перейдем к публикации приложения на хостинг. Я использую windows хостинг reg.ru. После покупки хостинга и домена должно пройти некоторое время, пока данные обновятся в dns серверах. Обычно достаточно несколько часов, но я предпочитаю выждать сутки.
Авторизуемся на сайте провайдера и переходим в меню мои хостинг и услуги
Далее в таблице нажимаем на ссылку войти в столбце Панель управления
В панели управления сайта выбираем ftp доступ
Добавляем нового пользователя
Задаем логин, пароль, указываем папку для публикации и обязательно ставим галочки на права чтения и записи.
Теперь идем в Visual Studio и начинаем настраивать публикацию.
В контекстном меню проекта выбираем пункт опубликовать
Выбираем FTP публикацию и нажимаем кнопку опубликовать (publish)
Задаем настройки подключения и нажимаем сохранить. После этого будет выполнена компиляция приложения о все необходимые файлы будут добавлены на сервер.
Если все настройки были выполнены правильно, то приложение будет доступно для использования.
Заключение
На этом мы завершаем небольшой цикл статей посвященный разработке angular 5 приложений. В дальнейшем я планирую вернуться к данной теме. Надеюсь было достаточно интересно и полезно. Исходный код доступен в репозитории github.