Дополнительные статьи

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

Последнее обновление: 17.11.2023

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

Вначале создадим новый каталог для нашего приложения. Пусть он называется gridapp. Определим в проекте файл package.json:

{
  "name": "gridapp",
  "version": "1.0.0",
  "description": "Grid Angular 17 Project",
  "author": "Eugene Popov metanit.com",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build"
  },
    "dependencies": {
    "@angular/common": "~17.0.0",
    "@angular/compiler": "~17.0.0",
    "@angular/core": "~17.0.0",
    "@angular/forms": "~17.0.0",
    "@angular/platform-browser": "~17.0.0",
    "@angular/platform-browser-dynamic": "~17.0.0",
    "@angular/router": "~17.0.0",
    "rxjs": "~7.8.0",
    "zone.js": "~0.14.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~17.0.0",
    "@angular/cli": "~17.0.0",
    "@angular/compiler-cli": "~17.0.0",
    "typescript": "~5.2.2"
  }
}

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

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

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "module": "ES2022",
    "moduleResolution": "node",
    "target": "ES2022",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "ES2022",
      "dom"
    ]
  },
    "files": [ "src/main.ts" ],
    "include": [ "src/**/*.d.ts"]
}

И также добавим в проект файл angular.json:

{
  "version": 1,
  "projects": {
    "gridapp": {
      "projectType": "application",
      "root": "",
      "sourceRoot": "src",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": ["zone.js"],
            "tsConfig": "tsconfig.json",
            "aot": true
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "buildTarget": "gridapp:build"
          }
        }
      }
    }
  }
}

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

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

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

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

import {Injectable} from "@angular/core";
import {HttpClient, HttpHeaders} from "@angular/common/http";
import {User} from "./user";
    
@Injectable()
export class UserService{
    
    private url = "http://localhost:3000/api/users";
    constructor(private http: HttpClient){ }
       
    getUsers(){
        return this.http.get<Array<User>>(this.url);
    }
   
    createUser(user: User){
        const myHeaders = new HttpHeaders().set("Content-Type", "application/json");
        return this.http.post<User>(this.url, JSON.stringify(user), {headers: myHeaders}); 
    }
    updateUser(user: User) {
        const myHeaders = new HttpHeaders().set("Content-Type", "application/json");
        return this.http.put<User>(this.url, JSON.stringify(user), {headers:myHeaders});
    }
    deleteUser(id: string){
	
        return this.http.delete<User>(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 {Component, OnInit, TemplateRef, ViewChild} from "@angular/core";
import {NgTemplateOutlet} from "@angular/common";
import { FormsModule }   from "@angular/forms";
import { HttpClientModule }   from "@angular/common/http";
import {User} from "./user";
import {UserService} from "./user.service";
    
@Component({ 
    selector: "my-app", 
    standalone: true,
    imports: [FormsModule, HttpClientModule, NgTemplateOutlet],
    templateUrl: "./app.component.html",
    styles:`
    td, th {padding:3px;min-width:180px;max-width:200px;}
    input {width:100%}
    `,
    providers: [UserService]
}) 
export class AppComponent implements OnInit {
    //типы шаблонов
    @ViewChild("readOnlyTemplate", {static: false}) readOnlyTemplate: TemplateRef<any>|undefined;
    @ViewChild("editTemplate", {static: false}) editTemplate: TemplateRef<any>|undefined;
        
    editedUser: User|null = null;
    users: Array<User>;
    isNewRecord: boolean = false;
    statusMessage: string = "";
        
    constructor(private serv: UserService) {
        this.users = new Array<User>();
    }
        
    ngOnInit() {
        this.loadUsers();
    }
        
    //загрузка пользователей
    private loadUsers() {
        this.serv.getUsers().subscribe((data: Array<User>) => {
                this.users = data; 
            });
    }
    // добавление пользователя
    addUser() {
        this.editedUser = new User("","",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 as User).subscribe(_ => {
                this.statusMessage = "Данные успешно добавлены",
                this.loadUsers();
            });
            this.isNewRecord = false;
            this.editedUser = null;
        } else {
            // изменяем пользователя
            this.serv.updateUser(this.editedUser as User).subscribe(_ => {
                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(_ => {
            this.statusMessage = "Данные успешно удалены",
            this.loadUsers();
        });
    }
}

Так как каждая строка грида может быть в двух состояниях - в режиме редактирования и в режиме просмотра, то соответственно определяем с помощью декоратора ViewChild две переменных readOnlyTemplate and editTemplate, через которые мы будем ссылаться на используемые для строк шаблоны. Каждая переменная представляет тип TemplateRef | undefined. 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>
<button (click)="addUser()">Добавить</button>
<table>
    
    <tr>
        <th>Id</th>
        <th>Имя</th>
        <th>Возраст</th>
        <th></th>
    </tr>
    @for(user of users; track $index){
        <tr>
            <ng-template [ngTemplateOutlet]="loadTemplate(user)" [ngTemplateOutletContext]="{ $implicit: user}">
            </ng-template>
        </tr>
    }
</table>
<div>{{statusMessage}}</div>
    
    
<!--шаблон для чтения-->
<ng-template #readOnlyTemplate let-user>
    <td>{{user._id}}</td>
    <td>{{user.name}}</td>
    <td>{{user.age}}</td>
    <td>
        <button (click)="editUser(user)">Изменить</button> 
        <button (click)="deleteUser(user)">Удалить</button>
    </td>
</ng-template>
   
<!--шаблон для редактирования-->
<ng-template #editTemplate>
    <td>
        <input [(ngModel)]="editedUser._id" readonly disabled />
    </td>
    <td>
        <input [(ngModel)]="editedUser.name" />
    </td>
    <td>
        <input type="number" [(ngModel)]="editedUser.age" />
    </td>
    <td>
        <button (click)="saveUser()">Сохранить</button>
        <button (click)="cancel()">Отмена</button>
    </td>
</ng-template>

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

 @for(user of users; track $index){
    <tr>
        <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 файл main.ts:

import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent);

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

<!DOCTYPE html>
<html>
    <head>
        <title>METANIT.COM</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        <my-app>Загрузка...</my-app>
    </body>
</html>

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

Grid in Angular 17

Создание серверной части

Для тестирования я определил приложение на node.js, которое для хранения данных использует бд mongodb. Для этого приложения создадим каталог и вначале определим в нем файл package.json со следующим содержимым:

{
  "name": "mongoapp",
  "version": "1.0.0",
  "dependencies": {
      "express": "~4.18.0",
      "mongodb": "~6.3.0"
    }
}

То есть приложение на node.js будет использовать два пакета: express для упрощения создания API и mongodb для работы с базой данных MongoDB. С помощью команды npm install установим эти пакеты.

Далее в папке приложения Node.js определим файл app.js, который собственно будет представлять приложение Node.js:

const express = require("express");
const MongoClient = require("mongodb").MongoClient;
const objectId = require("mongodb").ObjectId;
    
const app = express();
const jsonParser = express.json();
  
const mongoClient = new MongoClient("mongodb://127.0.0.1:27017/");

(async () => {
     try {
        await mongoClient.connect();
        app.locals.collection = mongoClient.db("usersdb").collection("users");
        app.listen(3000);
        console.log("Сервер ожидает подключения...");
    }catch(err) {
        return console.log(err);
    } 
})();
 
 
// настройка CORS
app.use((req, res, next) =>{
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Allow-Methods", "GET, PATCH, PUT, POST, DELETE, OPTIONS");
    next();  // передаем обработку запроса дальше
});

app.get("/api/users", async(req, res) => {
         
    const collection = req.app.locals.collection;
    try{
        const users = await collection.find({}).toArray();
        res.send(users);
    }
    catch(err){
        console.log(err);
        res.sendStatus(500);
    }
      
});
app.get("/api/users/:id", async(req, res) => {
         
    const collection = req.app.locals.collection;
    try{
        const id = new objectId(req.params.id);
        const user = await collection.findOne({_id: id});
        if(user) res.send(user);
        else res.sendStatus(404);
    }
    catch(err){
        console.log(err);
        res.sendStatus(500);
    }
});
    
app.post("/api/users", jsonParser, async(req, res)=> {
        
    if(!req.body) return res.sendStatus(400);
        
    const userName = req.body.name;
    const userAge = req.body.age;
    const user = {name: userName, age: userAge};
        
    const collection = req.app.locals.collection;
     
    try{
        await collection.insertOne(user);
        res.send(user);
    }
    catch(err){
        console.log(err);
        res.sendStatus(500);
    }
});
     
app.delete("/api/users/:id", async(req, res)=>{
         
    const collection = req.app.locals.collection;
    try{
        const id = new objectId(req.params.id);
        const user = await collection.findOneAndDelete({_id: id});
        if(user) res.send(user);
        else res.sendStatus(404);
    }
    catch(err){
        console.log(err);
        res.sendStatus(500);
    }
});
    
app.put("/api/users", jsonParser, async(req, res)=>{
         
    if(!req.body) return res.sendStatus(400);
    const userName = req.body.name;
    const userAge = req.body.age;
        
    const collection = req.app.locals.collection;
    try{
        const id = new objectId(req.body._id);
        const user = await collection.findOneAndUpdate({_id: id}, { $set: {age: userAge, name: userName}},
         {returnDocument: "after" });

        if(user) res.send(user);
        else res.sendStatus(404);
    }
    catch(err){
        console.log(err);
        res.sendStatus(500);
    }
});
  
// прослушиваем прерывание работы программы (ctrl-c)
process.on("SIGINT", async() => {
     
    await mongoClient.close();
    console.log("Приложение завершило работу");
    process.exit();
});

Подробнее про взаимодействие node.js и mongodb можно прочитать здесь

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

Сначала перейдем в консоли к папке приложения Node.js и запустим сервер с помощью команды

node app.js

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

Grid с возможностью редактирования в Angular

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

Таблица в Angular 17
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850