Создание Angular 5 приложения. Часть 3

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.