Рассмотрим, как мы можем создать с помощью 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>
В итоге весь проект будет выглядеть следующим образом:
Для тестирования я определил приложение на 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:
При нажатии на кнопку "Изменить" для редеринга строка используется шаблон editTemplate, и объект становится доступен для редактирования: