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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
    "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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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, который будет описывать используемые данные:

1
2
3
4
5
6
7
export class User {
    constructor(
        public id: number,
        public name: string,
        public age: number
    ) {}
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
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 и который будет содержать следующий код:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<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.

1
2
3
4
5
6
7
<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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

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

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

1
2
import 'core-js';
import 'zone.js/dist/zone';

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!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 в виде следующего контроллера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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, и объект становится доступен для редактирования:

Скриншот

Комментарии