Создание таблицы студентов. Работа с локальным сервером
Создал список студентов во фронтенд части. В бэкэнд - сервер. В данном случае добавление студентов в таблицу, их получение из сервера и удаление осуществляется через fetch-запросы к серверу. Объект студента во фронтенд части реализован через class. Столкнулся с проблемой не функционирования сортировки и фильтрации студентов. До взаимодействия с сервером функции работали. Взаимодействие с сервером осуществляется: студенты добавляются, сохраняются и удаляются. Но не работает сортировка и фильтрация. Возможно подскажете что в коде не то?
Фронтенд часть: (некоторые части закоментированы как предыдущие варианты реализации)
// Создание класса для массива студентов и методов работы с ним
class Student {
constructor(id, name, surname, lastname, birthday, studyStart, faculty) {
this.id = id,
this.name = name;
this.lastname = lastname;
this.surname = surname;
this.studyStart = studyStart;
this.faculty = faculty;
this.birthday = birthday;
}
get FullName() {
return this.surname + " " + this.name + " " + this.lastname;
}
convertBirthDayString() {
let day = this.birthday.getDate();
day = day < 10 ? "0" + day : day;
let month = this.birthday.getMonth() + 1;
month = month < 10 ? "0" + month : month;
const year = this.birthday.getFullYear();
return day + "." + month + "." + year;
}
getAgeStudent() {
const today = new Date();
let age = today.getFullYear() - this.birthday.getFullYear();
let m = today.getMonth() - this.birthday.getMonth();
if (m < 0 || (m === 0 && today.getDate() < this.birthday.getDate())) {
age--;
}
return ` (${age} лет)`;
}
getStudyTime() {
let currentTime = new Date();
// corse = today.getFullYear() -
if (
(currentTime.getMonth() + 1 >= 9 &&
currentTime.getFullYear() - this.studyStart > 4) ||
currentTime.getFullYear() - this.studyStart > 4
) {
return `${+this.studyStart} - ${+this.studyStart + 4} (закончил)`;
} else {
return `${+this.studyStart} - ${+this.studyStart + 4} (${
currentTime.getFullYear() - this.studyStart + 1
}-й курс)`;
}
}
}
// Функция подсчета срока учебы на текущую дату перенесена в класс
// getStudiesPeriod() {
// const currentTime = new Date();
// return currentTime.getFullYear() - this.studyStart;
// }
const SERVER_URL = "http://localhost:3000";
// ***ФУНКЦИЯ ДОБАВЛЕНИЯ СТУДЕНТА через СЕРВЕР
async function serverAddStudent(obj) {
let response = await fetch(SERVER_URL + "/api/students", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(obj),
});
let data = await response.json();
return data;
}
// ФУНКЦИЯ получения студентов с сервера
async function serverGetStudent() {
let response = await fetch(SERVER_URL + "/api/students", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
let data = await response.json();
return data;
}
// УДАЛЕНИЕ СТУДЕНТА
async function serverDeleteStudent(id) {
let response = await fetch(SERVER_URL + "/api/students/" + id, {
method: "DELETE",
});
let data = await response.json();
return data;
}
// let studentsList = [];
// (async () => {
// let serverData = await serverGetStudent();
// if (serverData) {
// for (const student of serverData) {
// studentsList.push(
// new Student(
// student.id,
// student.name,
// student.surname,
// student.lastname,
// new Date(student.birthday),
// student.studyStart,
// student.faculty
// )
// );
// }
// }
// })();
let studentsList = [];
serverGetStudent().then(students => {
studentsList = students
render();
})
// const $studentsList = document.getElementById("student-list");
const $studentsListTHALL = document.querySelectorAll(".studentTable th");
$filterForm = document.getElementById("filter-form");
$validationForm = document.getElementById("add-student");
$fullNameFilterInp = document.getElementById("filter-fio");
$facultyFilterInp = document.getElementById("filter-faculty");
$studyStartFilterInp = document.getElementById("filter-studyStart");
$studyFinishFilterInp = document.getElementById("filter-studyFinish");
($inputName = document.getElementById("input-name")),
($inputSurname = document.getElementById("input-surname")),
($inputLastname = document.getElementById("input-lastname")),
($inputFaculty = document.getElementById("input-faculty")),
($inputBirthday = new Date(document.getElementById("input-birthday"))),
($inputStudyStart = Number(document.getElementById("input-studyStart")));
let column = "FullName",
columnDir = true;
// Получаем TR (таблицы) студентов
function newStudentTR(student) {
const $studentTR = document.createElement("tr");
($fioTD = document.createElement("td")),
($birthdayTD = document.createElement("td")),
($facultyTD = document.createElement("td")),
($studyStartTD = document.createElement("td"));
$deleteTD = document.createElement("td");
$deleteBtn = document.createElement("button");
$deleteBtn.classList.add("btn", "btn-danger");
$deleteBtn.textContent = "Удалить";
$fioTD.textContent = student.FullName;
$facultyTD.textContent = student.faculty;
$birthdayTD.textContent =
student.convertBirthDayString() + student.getAgeStudent();
$studyStartTD.textContent = student.getStudyTime();
student.studyFinish = $studyStartTD.textContent.match(/\d+/g)[1];
$deleteBtn.addEventListener("click", async function () {
await serverDeleteStudent(student.id);
$studentTR.remove();
});
$deleteTD.append($deleteBtn);
$studentTR.append($fioTD);
$studentTR.append($birthdayTD);
$studentTR.append($facultyTD);
$studentTR.append($studyStartTD);
$studentTR.append($deleteTD);
return $studentTR;
}
// Сортировка массива по параметрам
function getSortStudents(prop, dir) {
const sortedStudentsList = [...studentsList];
return sortedStudentsList.sort((studentA, studentB) =>
!dir
? studentA[prop] > studentB[prop]
: studentA[prop] < studentB[prop]
? -1
: 1
);
}
function filterStudents(studentsList, prop, value) {
const filteredArr = [];
const copyArr = [...studentsList];
for (const student of copyArr) {
if (String(student[prop]).includes(value)) filteredArr.push(student);
}
return filteredArr;
}
// Отрисовка таблицы студентов
async function render() {
let studentsListCopy = [...studentsList];
const $studentsList = document.getElementById("student-list");
$studentsList.innerHTML = "";
for (const student of studentsListCopy) {
$studentsList.append(
newStudentTR(
new Student(
student.id,
student.name,
student.surname,
student.lastname,
new Date(student.birthday),
student.studyStart,
student.faculty
)
)
);
}
studentsListCopy = getSortStudents(column, columnDir);
// События фильтрации
const FullNameVal = document.getElementById("filter-fio").value;
facultyVal = document.getElementById("filter-faculty").value;
studyStartVal = document.getElementById("filter-studyStart").value;
studyFinishVal = document.getElementById("filter-studyFinish").value;
if (FullNameVal !== "")
studentsListCopy = filterStudents(
studentsListCopy,
"FullName",
FullNameVal
);
if (facultyVal !== "")
studentsListCopy = filterStudents(studentsListCopy, "faculty", facultyVal);
if (studyStartVal !== "" && studyStartVal.length > 3)
studentsListCopy = filterStudents(
studentsListCopy,
"studyStart",
studyStartVal
);
if (studyFinishVal !== "" && studyFinishVal.length > 3)
studentsListCopy = filterStudents(
studentsListCopy,
"studyFinish",
studyFinishVal
);
}
render();
// События сортировки
$studentsListTHALL.forEach((element) => {
element.addEventListener("click", function () {
column = this.dataset.column;
columnDir = !columnDir;
render(studentsList);
});
});
// Добавление
$validationForm.addEventListener("submit", async function (event) {
event.preventDefault();
let serverDataObj = await serverAddStudent({
name: document.getElementById("input-name").value,
surname: document.getElementById("input-surname").value,
lastname: document.getElementById("input-lastname").value,
birthday: new Date(document.getElementById("input-birthday").value),
studyStart: Number(document.getElementById("input-studyStart").value),
faculty: document.getElementById("input-faculty").value,
});
if (validation(this) == true) {
alert("Форма успешно заполнена");
studentsList.push(
new Student(
serverDataObj.id,
serverDataObj.name,
serverDataObj.surname,
serverDataObj.lastname,
new Date(serverDataObj.birthday),
serverDataObj.studyStart,
serverDataObj.faculty
)
);
}
render();
});
// ВАЛИДАЦИЯ ФОРМЫ
function validation($validationForm) {
function removeError(input) {
const parent = input.closest(".input-box");
if (parent.classList.contains("error")) {
parent.querySelector(".error-label").remove();
parent.classList.remove("error");
}
}
function createError(input, text) {
const parent = input.closest(".input-box");
const errorLabel = document.createElement("label");
errorLabel.classList.add("error-label");
errorLabel.textContent = text;
parent.classList.add("error");
parent.append(errorLabel);
}
let result = true;
$validationForm.querySelectorAll("input").forEach((input) => {
removeError(input);
if (input.dataset.required == "true") {
if (input.value == "") {
removeError(input);
createError(input, "Поле не заполнено!");
result = false;
}
}
if (input.dataset.minTerm) {
if (input.value != "" && input.value < input.dataset.minTerm) {
removeError(input);
createError(
input,
`Введите год поступления, начиная с ${input.dataset.minTerm}`
);
result = false;
}
}
});
return result;
}
$filterForm.addEventListener("submit", function (event) {
event.preventDefault();
render();
});
$fullNameFilterInp.addEventListener("input", function () {
render();
});
$facultyFilterInp.addEventListener("input", function () {
render();
});
$studyStartFilterInp.addEventListener("input", function () {
render();
});
$studyFinishFilterInp.addEventListener("input", function () {
render();
});
render();
Бэкэнд часть
* eslint-disable no-console */
// импорт стандартных библиотек Node.js
const { existsSync, readFileSync, writeFileSync } = require('fs');
const { createServer } = require('http');
// файл для базы данных
const DB_FILE = process.env.DB_FILE || './db.json';
// номер порта, на котором будет запущен сервер
const PORT = process.env.PORT || 3000;
// префикс URI для всех методов приложения
const URI_PREFIX = '/api/students';
/**
* Класс ошибки, используется для отправки ответа с определённым кодом и описанием ошибки
*/
class ApiError extends Error {
constructor(statusCode, data) {
super();
this.statusCode = statusCode;
this.data = data;
}
}
/**
* Асинхронно считывает тело запроса и разбирает его как JSON
* @param {Object} req - Объект HTTP запроса
* @throws {ApiError} Некорректные данные в аргументе
* @returns {Object} Объект, созданный из тела запроса
*/
function drainJson(req) {
return new Promise((resolve) => {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
resolve(JSON.parse(data));
});
});
}
/**
* Проверяет входные данные и создаёт из них корректный объект студента
* @param {Object} data - Объект с входными данными
* @throws {ApiError} Некорректные данные в аргументе (statusCode 422)
* @returns {{ name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string }} Объект студента
*/
function makeStudentFromData(data) {
const errors = [];
function asString(v) {
return v && String(v).trim() || '';
}
// составляем объект, где есть только необходимые поля
const student = {
name: asString(data.name),
surname: asString(data.surname),
lastname: asString(data.lastname),
birthday: asString(data.birthday),
studyStart: asString(data.studyStart),
faculty: asString(data.faculty),
}
// проверяем, все ли данные корректные и заполняем объект ошибок, которые нужно отдать клиенту
if (!student.name) errors.push({ field: 'name', message: 'Не указано имя' });
if (!student.surname) errors.push({ field: 'surname', message: 'Не указана фамилия' });
if (!student.lastname) errors.push({ field: 'lastname', message: 'Не указано отчество' });
if (!student.birthday) errors.push({ field: 'birthday', message: 'Не указана дата рождения' });
if (!student.studyStart) errors.push({ field: 'studyStart', message: 'Не указано начало обучения' });
if (!student.faculty) errors.push({ field: 'faculty', message: 'Не указан факультет' });
// если есть ошибки, то бросаем объект ошибки с их списком и 422 статусом
if (errors.length) throw new ApiError(422, { errors });
return student;
}
/**
* Возвращает список студентов из базы данных
* @param {{ search: string }} [params] - Поисковая строка
* @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string }[]} Массив студентов
*/
function getStudentList(params = {}) {
const students = JSON.parse(readFileSync(DB_FILE) || '[]');
if (params.search) {
const search = params.search.trim().toLowerCase();
return students.filter(student => [
student.name,
student.surname,
student.lastname,
student.birthday,
student.studyStart,
student.faculty,
]
.some(str => str.toLowerCase().includes(search))
);
}
return students;
}
/**
* Создаёт и сохраняет студента в базу данных
* @throws {ApiError} Некорректные данные в аргументе, студент не создан (statusCode 422)
* @param {Object} data - Данные из тела запроса
* @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
*/
function createStudent(data) {
const newItem = makeStudentFromData(data);
newItem.id = Date.now().toString();
newItem.createdAt = newItem.updatedAt = new Date().toISOString();
writeFileSync(DB_FILE, JSON.stringify([...getStudentList(), newItem]), { encoding: 'utf8' });
return newItem;
}
/**
* Возвращает объект студента по его ID
* @param {string} itemId - ID студента
* @throws {ApiError} Студент с таким ID не найден (statusCode 404)
* @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
*/
function getStudent(itemId) {
const student = getStudentList().find(({ id }) => id === itemId);
if (!student) throw new ApiError(404, { message: 'Student Not Found' });
return student;
}
/**
* Изменяет студента с указанным ID и сохраняет изменения в базу данных
* @param {string} itemId - ID изменяемого студента
* @param {{ name?: string, surname?: string, lastname?: string, birthday?: string, studyStart?: string, faculty?: string }} data - Объект с изменяемыми данными
* @throws {ApiError} Студент с таким ID не найден (statusCode 404)
* @throws {ApiError} Некорректные данные в аргументе (statusCode 422)
* @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
*/
function updateStudent(itemId, data) {
const students = getStudentList();
const itemIndex = students.findIndex(({ id }) => id === itemId);
if (itemIndex === -1) throw new ApiError(404, { message: 'Student Not Found' });
Object.assign(students[itemIndex], makeStudentFromData({...students[itemIndex], ...data }));
students[itemIndex].updatedAt = new Date().toISOString();
writeFileSync(DB_FILE, JSON.stringify(students), { encoding: 'utf8' });
return students[itemIndex];
}
/**
* Удаляет студента из базы данных
* @param {string} itemId - ID студента
* @returns {{}}
*/
function deleteStudent(itemId) {
const students = getStudentList();
const itemIndex = students.findIndex(({ id }) => id === itemId);
if (itemIndex === -1) throw new ApiError(404, { message: 'Student Not Found' });
students.splice(itemIndex, 1);
writeFileSync(DB_FILE, JSON.stringify(students), { encoding: 'utf8' });
return {};
}
// создаём новый файл с базой данных, если он не существует
if (!existsSync(DB_FILE)) writeFileSync(DB_FILE, '[]', { encoding: 'utf8' });
// создаём HTTP сервер, переданная функция будет реагировать на все запросы к нему
module.exports = createServer(async(req, res) => {
// req - объект с информацией о запросе, res - объект для управления отправляемым ответом
// этот заголовок ответа указывает, что тело ответа будет в JSON формате
res.setHeader('Content-Type', 'application/json');
// CORS заголовки ответа для поддержки кросс-доменных запросов из браузера
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// запрос с методом OPTIONS может отправлять браузер автоматически для проверки CORS заголовков
// в этом случае достаточно ответить с пустым телом и этими заголовками
if (req.method === 'OPTIONS') {
// end = закончить формировать ответ и отправить его клиенту
res.end();
return;
}
// если URI не начинается с нужного префикса - можем сразу отдать 404
if (!req.url || !req.url.startsWith(URI_PREFIX)) {
res.statusCode = 404;
res.end(JSON.stringify({ message: 'Not Found' }));
return;
}
// убираем из запроса префикс URI, разбиваем его на путь и параметры
const [uri, query] = req.url.substr(URI_PREFIX.length).split('?');
const queryParams = {};
// параметры могут отсутствовать вообще или иметь вид a=b&b=c
// во втором случае наполняем объект queryParams { a: 'b', b: 'c' }
if (query) {
for (const piece of query.split('&')) {
const [key, value] = piece.split('=');
queryParams[key] = value ? decodeURIComponent(value) : '';
}
}
try {
// обрабатываем запрос и формируем тело ответа
const body = await (async() => {
if (uri === '' || uri === '/') {
// /api/students
if (req.method === 'GET') return getStudentList(queryParams);
if (req.method === 'POST') {
const createdItem = createStudent(await drainJson(req));
res.statusCode = 201;
res.setHeader('Access-Control-Expose-Headers', 'Location');
res.setHeader('Location', `${URI_PREFIX}/${createdItem.id}`);
return createdItem;
}
} else {
// /api/students/{id}
// параметр {id} из URI запроса
const itemId = uri.substr(1);
if (req.method === 'GET') return getStudent(itemId);
if (req.method === 'PATCH') return updateStudent(itemId, await drainJson(req));
if (req.method === 'DELETE') return deleteStudent(itemId);
}
return null;
})();
res.end(JSON.stringify(body));
} catch (err) {
// обрабатываем сгенерированную нами же ошибку
if (err instanceof ApiError) {
res.writeHead(err.statusCode);
res.end(JSON.stringify(err.data));
} else {
// если что-то пошло не так - пишем об этом в консоль и возвращаем 500 ошибку сервера
res.statusCode = 500;
res.end(JSON.stringify({ message: 'Server Error' }));
console.error(err);
}
}
})
// выводим инструкцию, как только сервер запустился...
.on('listening', () => {
if (process.env.NODE_ENV !== 'test') {
console.log(`Сервер Students запущен. Вы можете использовать его по адресу http://localhost:${PORT}`);
console.log('Нажмите CTRL+C, чтобы остановить сервер');
console.log('Доступные методы:');
console.log(`GET ${URI_PREFIX} - получить список студентов, в query параметр search можно передать поисковый запрос`);
console.log(`POST ${URI_PREFIX} - создать студента, в теле запроса нужно передать объект { name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string}`);
console.log(`GET ${URI_PREFIX}/{id} - получить студента по его ID`);
console.log(`PATCH ${URI_PREFIX}/{id} - изменить студента с ID, в теле запроса нужно передать объект { name?: string, surname?: string, lastname?: string, birthday?: string, studyStart?: string, faculty?: string}`);
console.log(`DELETE ${URI_PREFIX}/{id} - удалить студента по ID`);
}
})
// ...и вызываем запуск сервера на указанном порту
.listen(PORT);
Ответы (1 шт):
На самом деле все было просто: фильтрация и сортировка должны происходить до отображения всех tr
в функции render()
(отрисовка таблицы студентов).
Цикл for
должен быть после всех присвоений данных в studentsListCopy()
.