Перейти к содержанию

Grid и CRUD-операции

Рассмотрим, как мы можем создать с помощью Angular подобие грида для вывода данных и совместить его с базовыми операциями по управлению данными.

Вначале создадим новый проект. Определим в проекте файл package.json:

{
  "name": "gridapp",
  "version": "1.0.0",
  "description": "Grid Angular 8 Project",
  "author": "metanit.com",
  "scripts": {
    "dev": "webpack-dev-server --hot --open",
    "build": "webpack"
  },
  "dependencies": {
    "@angular/common": "~8.0.0",
    "@angular/compiler": "~8.0.0",
    "@angular/core": "~8.0.0",
    "@angular/forms": "~8.0.0",
    "@angular/platform-browser": "~8.0.0",
    "@angular/platform-browser-dynamic": "~8.0.0",
    "@angular/router": "~8.0.0",
    "core-js": "^3.1.0",
    "rxjs": "^6.5.0",
    "zone.js": "^0.9.0"
  },
  "devDependencies": {
    "@types/node": "^12.0.0",
    "typescript": "^3.5.0",
    "webpack": "^4.33.0",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.6.0",
    "angular2-template-loader": "^0.6.2",
    "awesome-typescript-loader": "^5.2.1",
    "html-loader": "^0.5.5"
  }
}

И затем установим все пакеты с помощью команды npm install.

Далее добавим в проект файл tsconfig.json с конфигурацией TypeScript:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots": ["node_modules/@types/"]
  },
  "exclude": ["node_modules"]
}

И также добавим в проект файл webpack.config.js:

const path = require('path')
const webpack = require('webpack')
module.exports = {
  entry: {
    polyfills: './src/polyfills.ts',
    app: './src/main.ts'
  },
  output: {
    path: path.resolve(__dirname, './public'), // путь к каталогу выходных файлов - папка public
    publicPath: '/public/',
    filename: '[name].js' // название создаваемого файла
  },
  devServer: {
    historyApiFallback: true
  },
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      //загрузчик для ts
      {
        test: /\.ts$/, // определяем тип файлов
        use: [
          {
            loader: 'awesome-typescript-loader',
            options: {
              configFileName: path.resolve(__dirname, 'tsconfig.json')
            }
          },
          'angular2-template-loader'
        ]
      },
      {
        test: /\.html$/,
        loader: 'html-loader'
      }
    ]
  },
  plugins: [
    new webpack.ContextReplacementPlugin(
      /angular(\\|\/)core/,
      path.resolve(__dirname, 'src'), // каталог с исходными файлами
      {} // карта маршрутов
    )
  ]
}

Затем в проекте создадим папку src. А в этой папке создадим каталог app и в начале определим в нем файл user.ts, который будет описывать используемые данные:

export class User {
  constructor(public id: number, public name: string, public age: number) {}
}

Класс User представляет пользователя и содержит три общедоступных поля id (уникальный идентификатор), name (имя) и age (возраст).

Все данные, описываемые классом User, будут храниться на сервере в базе данных. Поэтому нам необходим сервис для взаимодействия с сервером. И для этой цели в папке src/app создадим новый файл user.service.ts, в котором определим класс UserService:

import { Injectable } from '@angular/core'
import { HttpClient, HttpParams } from '@angular/common/http'
import { User } from './user'

@Injectable()
export class UserService {
  private url = 'http://localhost:63333/api/users'
  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get(this.url)
  }

  createUser(user: User) {
    return this.http.post(this.url, user)
  }
  updateUser(id: number, user: User) {
    const urlParams = new HttpParams().set('id', id.toString())
    return this.http.put(this.url, user, { params: urlParams })
  }
  deleteUser(id: number) {
    return this.http.delete(this.url + '/' + id)
  }
}

Для сервиса определен url для всех запросов. По этому url будет запущено приложение сервера. Оно может представлять любую серверную технологию: PHP, Node.js, ASP.NET. Для отправки запросов GET/POST/PUT/DELETE сервис использует соответствующие методы get()/post()/put()/delete() из объета http.

Далее добавим в папку src/app файл компонента app.component.ts:

import { TemplateRef, ViewChild } from '@angular/core'
import { Component, OnInit } from '@angular/core'
import { User } from './user'
import { UserService } from './user.service'
import { Observable } from 'rxjs'

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  providers: [UserService]
})
export class AppComponent implements OnInit {
  //типы шаблонов
  @ViewChild('readOnlyTemplate', { static: false })
  readOnlyTemplate: TemplateRef<any>
  @ViewChild('editTemplate', { static: false }) editTemplate: TemplateRef<any>

  editedUser: User
  users: Array<User>
  isNewRecord: boolean
  statusMessage: string

  constructor(private serv: UserService) {
    this.users = new Array<User>()
  }

  ngOnInit() {
    this.loadUsers()
  }

  //загрузка пользователей
  private loadUsers() {
    this.serv.getUsers().subscribe((data: User[]) => {
      this.users = data
    })
  }
  // добавление пользователя
  addUser() {
    this.editedUser = new User(0, '', 0)
    this.users.push(this.editedUser)
    this.isNewRecord = true
  }

  // редактирование пользователя
  editUser(user: User) {
    this.editedUser = new User(user.id, user.name, user.age)
  }
  // загружаем один из двух шаблонов
  loadTemplate(user: User) {
    if (this.editedUser && this.editedUser.id == user.id) {
      return this.editTemplate
    } else {
      return this.readOnlyTemplate
    }
  }
  // сохраняем пользователя
  saveUser() {
    if (this.isNewRecord) {
      // добавляем пользователя
      this.serv.createUser(this.editedUser).subscribe(data => {
        ;(this.statusMessage = 'Данные успешно добавлены'), this.loadUsers()
      })
      this.isNewRecord = false
      this.editedUser = null
    } else {
      // изменяем пользователя
      this.serv
        .updateUser(this.editedUser.id, this.editedUser)
        .subscribe(data => {
          ;(this.statusMessage = 'Данные успешно обновлены'), this.loadUsers()
        })
      this.editedUser = null
    }
  }
  // отмена редактирования
  cancel() {
    // если отмена при добавлении, удаляем последнюю запись
    if (this.isNewRecord) {
      this.users.pop()
      this.isNewRecord = false
    }
    this.editedUser = null
  }
  // удаление пользователя
  deleteUser(user: User) {
    this.serv.deleteUser(user.id).subscribe(data => {
      ;(this.statusMessage = 'Данные успешно удалены'), this.loadUsers()
    })
  }
}

Так как каждая строка грида может быть в двух состояниях - в режиме редактирования и в режиме просмотра, то соответственно определяем с помощью декоратора ViewChild две переменных readOnlyTemplate и editTemplate, через которые мы будем ссылаться на используемые для строк шаблоны. Каждая переменная представляет тип TemplateRef. TemplateRef используется для создания вложенных представлений.

Для хранения редактируемого пользователя определена переменная editedUser, а для хранения списка пользователей - переменная users.

В методе ngOnInit вызывается метод loadUsers, в котором происходит загрузка данных с помощью сервиса UserService в список users.

В методе addUser() добавляется новый объект User. При этом добавляемый объект помещается в переменную editedUser и затем добавляется в массив users. И кроме того, для переменной isNewRecord устанавливается значение true. Это позволит идентифицировать в дальнейшем объект как именно как объект для добавления.

Метод editUser() получает объект User, который надо отредактировать, и передает его переменной editedUser.

Метод loadTemplate() позволяет загрузить для определенного объекта User нужный шаблон. То есть, как было сказано выше, строка грида может находиться в двух состояниях, и соответственно у нас будет два шаблона: для просмотра и для редактирования. Объект, для которого надо загрузить шаблон, передается в качестве параметра. И если определена переменная editedUser и ее свойство Id совпадает со значением свойства Id у того объекта, для которого надо загрузить шаблон, то выбирается шаблон для редактирования. Иначе же загружается шаблон для просмотра.

В методе saveUser() в зависимости от значения переменной isNewRecord данные отправляются на сервер либо через запрос типа POST (добавление нового объекта), либо через запрос типа PUT (редактирование объекта).

Метод cancel() сбрасывает редактирование.

И метод deleteUser() удаляет объект, отправляя через сервис UserService запрос к серверу.

И также добавим в проект в папку src/app новый файл app.component.html, который будет представлять шаблон для компонента AppComponent и который будет содержать следующий код:

<h1>Список пользователей</h1>
<input
  type="button"
  value="Добавить"
  class="btn btn-default"
  (click)="addUser()"
/>
<table class="table table-striped">
  <thead>
    <tr>
      <td>Id</td>
      <td>Имя</td>
      <td>Возраст</td>
      <td></td>
      <td></td>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let user of users">
      <ng-template
        [ngTemplateOutlet]="loadTemplate(user)"
        [ngTemplateOutletContext]="{ $implicit: user}"
      >
      </ng-template>
    </tr>
  </tbody>
</table>
<div>{{statusMessage}}</div>

<!--шаблон для чтения-->
<ng-template #readOnlyTemplate let-user>
  <td>{{user.id}}</td>
  <td>{{user.name}}</td>
  <td>{{user.age}}</td>
  <td>
    <input
      type="button"
      value="Изменить"
      class="btn btn-default"
      (click)="editUser(user)"
    />
  </td>
  <td>
    <input
      type="button"
      value="Удалить"
      (click)="deleteUser(user)"
      class="btn btn-danger"
    />
  </td>
</ng-template>

<!--шаблон для редактирования-->
<ng-template #editTemplate>
  <td>
    <input
      type="text"
      [(ngModel)]="editedUser.id"
      readonly
      disabled
      class="form-control"
    />
  </td>
  <td>
    <input type="text" [(ngModel)]="editedUser.name" class="form-control" />
  </td>
  <td>
    <input type="text" [(ngModel)]="editedUser.age" class="form-control" />
  </td>
  <td>
    <input
      type="button"
      value="Сохранить"
      (click)="saveUser()"
      class="btn btn-success"
    />
  </td>
  <td>
    <input
      type="button"
      value="Отмена"
      (click)="cancel()"
      class="btn btn-warning"
    />
  </td>
</ng-template>

С помощью директивы ngFor для каждого объекта из массива users создается строку с нужным шаблоном. Для встраивания шаблона в строку применяется элемент ng-template.

<tr *ngFor="let user of users">
  <ng-template
    [ngTemplateOutlet]="loadTemplate(user)"
    [ngTemplateOutletContext]="{ $implicit: user}"
  >
  </ng-template>
</tr>

С помощью директивы ngTemplateOutlet встраивается шаблон, который представляет объект TemplateRef. Эта директива привязана к методу loadTemplate(), который определен в классе AppComponent и который возвращает определенный шаблон.

А свойство ngTemplateOutletContext для передачи контекста в шаблон. С помощью параметра $implicit задается передаваемый объект. В данном случае это объект user.

В конце файла определены два шаблона для строк грида: readOnlyTemplate и editTemplate. Для определения шаблонов Angular использует элемент ng-template.

Шаблон readOnlyTemplate отображает объект User в режиме для чтения. Он содержит кнопки для редактирования и удаления объекта. Шаблон editTemplate определяет текстовые поля, которые привязаны к свойствам переменной editedUser из класса AppComponent. И также шаблон содержит кнопки для сохранения и отмены операции.

И также определим в папке src/app файл модуля приложения app.module.ts:

import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { HttpClientModule } from '@angular/common/http'
import { AppComponent } from './app.component'
@NgModule({
  imports: [BrowserModule, FormsModule, HttpClientModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

В папке src определим файл main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
const platform = platformBrowserDynamic()
platform.bootstrapModule(AppModule)

Также добавим в папку src файл polyfills.ts:

import 'core-js'
import 'zone.js/dist/zone'

И в конце определим в проекте в корневой папке проекта главную веб-страницу index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello Angular 8</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"
    />
  </head>
  <body>
    <my-app>Загрузка...</my-app>
    <script src="public/polyfills.js"></script>
    <script src="public/app.js"></script>
  </body>
</html>

В итоге весь проект будет выглядеть следующим образом:

Структура проекта

Для тестирования я определил приложение на ASP NET Core Web API в виде следующего контроллера:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AngularMvcService.Models;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace AngularMvcService.Controllers {
  [Route("api/[controller]")][enablecors("allowallorigin")]
  public class UsersController : Controller {
    ApplicationContext db;

    public UsersController(ApplicationContext context) {
      db = context;
    }

    [HttpGet]
    public IEnumerable<User> Get(){
      return db.Users.ToList();
    }

    // GET api/users/5
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        User user = db.Users.FirstOrDefault(x => x.Id == id);
        if (user == null)
            return NotFound();
        return new ObjectResult(user);
    }

    // POST api/users
    [HttpPost]
    public IActionResult Post([FromBody]User user)
    {
        if (user == null)
        {
            return BadRequest();
        }

        db.Users.Add(user);
        db.SaveChanges();
        return Ok(user);
    }

    // PUT api/users/
    [HttpPut]
    public IActionResult Put([FromBody]User user)
    {
        if (user == null)
        {
            return BadRequest();
        }
        if (!db.Users.Any(x => x.Id == user.Id))
        {
            return NotFound();
        }

        db.Update(user);
        db.SaveChanges();
        return Ok(user);
    }

    // DELETE api/users/5
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        User user = db.Users.FirstOrDefault(x => x.Id == id);
        if (user == null)
        {
            return NotFound();
        }
        db.Users.Remove(user);
        db.SaveChanges();
        return Ok(user);
    }
  }
/* используемая модель данных
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}*/
}

Но естественно для приложения уровня сервера можно использовать любую другую технологию бекэнда: PHP, Node.js, Java и т.д.

Поле запуска приложения на сервере запустим приложение Angular. Если сервер возвратит какие-либо данные, то будут отображены в таблице с помощью шаблона readOnlyTemplate:

Скриншот

При нажатии на кнопку "Изменить" для редеринга строка используется шаблон editTemplate, и объект становится доступен для редактирования:

Скриншот