AJAX submit для формы редактирования сотрудника, ошибки показываются inline
This commit is contained in:
parent
270407abf9
commit
2d08c8bc56
@ -1,84 +1,180 @@
|
||||
/**
|
||||
* employeeFormHandler.js
|
||||
* Минимальный стаб, оставленный ради обратной совместимости после
|
||||
* отказа от кастомного JS-сабмита. Теперь форма отправляется браузером
|
||||
* напрямую в фронтовую Django-вьюху, а модуль лишь возвращает
|
||||
* совместимое API без вмешательства в submit.
|
||||
* Обработка отправки формы редактирования сотрудника через AJAX
|
||||
* с отображением ошибок валидации inline без перезагрузки страницы
|
||||
*/
|
||||
export function initEmployeeForm(options = {}) {
|
||||
const formId = options.formId || 'apiEditForm';
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
if (!form) {
|
||||
console.warn('[EmployeeForm] Форма не найдена, инициализация пропущена');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Новый обработчик: submit через fetch
|
||||
form.addEventListener('submit', async function (e) {
|
||||
console.info('[EmployeeForm] Инициализация AJAX обработчика формы');
|
||||
|
||||
// Перехватываем submit формы
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
// Очищаем старые ошибки
|
||||
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||||
form.querySelectorAll('.invalid-feedback').forEach(el => el.textContent = '');
|
||||
|
||||
const endpoint = form.dataset.endpoint || form.action;
|
||||
const formData = new FormData(form);
|
||||
let payload = {};
|
||||
formData.forEach((value, key) => {
|
||||
if (payload[key]) {
|
||||
if (!Array.isArray(payload[key])) payload[key] = [payload[key]];
|
||||
payload[key].push(value);
|
||||
} else {
|
||||
payload[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Если есть файл, отправляем multipart, иначе json
|
||||
let fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': form.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
||||
}
|
||||
};
|
||||
if (form.enctype === 'multipart/form-data') {
|
||||
fetchOptions.body = formData;
|
||||
delete fetchOptions.headers['Content-Type'];
|
||||
} else {
|
||||
fetchOptions.headers['Content-Type'] = 'application/json';
|
||||
fetchOptions.body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const btnLabel = submitBtn?.querySelector('.btn-label');
|
||||
const btnSpinner = submitBtn?.querySelector('.btn-spinner');
|
||||
|
||||
// Очищаем предыдущие ошибки
|
||||
clearFormErrors(form);
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
if (btnLabel) btnLabel.classList.add('d-none');
|
||||
if (btnSpinner) btnSpinner.classList.remove('d-none');
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, fetchOptions);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
// Успех: можно обновить данные или закрыть форму
|
||||
if (options.onSuccess) options.onSuccess(data);
|
||||
else location.reload();
|
||||
} else {
|
||||
// Ошибки: показываем в форме
|
||||
if (data && typeof data === 'object') {
|
||||
for (const [field, errors] of Object.entries(data)) {
|
||||
const input = form.querySelector(`[name="${field}"]`);
|
||||
if (input) {
|
||||
input.classList.add('is-invalid');
|
||||
let feedback = input.parentElement.querySelector('.invalid-feedback');
|
||||
if (!feedback) {
|
||||
feedback = document.createElement('div');
|
||||
feedback.className = 'invalid-feedback';
|
||||
input.parentElement.appendChild(feedback);
|
||||
}
|
||||
feedback.textContent = Array.isArray(errors) ? errors.join(' ') : errors;
|
||||
}
|
||||
}
|
||||
const endpoint = form.dataset.endpoint || form.action;
|
||||
const formData = new FormData(form);
|
||||
|
||||
console.log('[EmployeeForm] Отправка данных на:', endpoint);
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': formData.get('csrfmiddlewaretoken') || ''
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('[EmployeeForm] Успешно сохранено');
|
||||
showSuccessMessage('Изменения сохранены');
|
||||
|
||||
// Обновляем страницу через 1 секунду
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
console.warn('[EmployeeForm] Ошибка валидации:', data);
|
||||
displayFormErrors(form, data);
|
||||
showErrorMessage('Исправьте ошибки в форме');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Ошибка отправки формы: ' + err);
|
||||
} catch (error) {
|
||||
console.error('[EmployeeForm] Ошибка отправки:', error);
|
||||
showErrorMessage('Произошла ошибка при сохранении');
|
||||
} finally {
|
||||
// Возвращаем кнопку в нормальное состояние
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
if (btnLabel) btnLabel.classList.remove('d-none');
|
||||
if (btnSpinner) btnSpinner.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
setSubmitting: (v) => {},
|
||||
setSubmitting: (state) => {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) submitBtn.disabled = state;
|
||||
},
|
||||
refresh: () => location.reload()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех ошибок в форме
|
||||
*/
|
||||
function clearFormErrors(form) {
|
||||
// Убираем класс is-invalid со всех полей
|
||||
form.querySelectorAll('.is-invalid').forEach(el => {
|
||||
el.classList.remove('is-invalid');
|
||||
});
|
||||
|
||||
// Очищаем все сообщения об ошибках
|
||||
form.querySelectorAll('.invalid-feedback').forEach(el => {
|
||||
el.textContent = '';
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отображение ошибок валидации в полях формы
|
||||
*/
|
||||
function displayFormErrors(form, errors) {
|
||||
// errors может быть объектом с полями или массивом строк
|
||||
if (Array.isArray(errors)) {
|
||||
// Общие ошибки (non_field_errors)
|
||||
showErrorMessage(errors.join(', '));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof errors === 'object') {
|
||||
Object.keys(errors).forEach(fieldName => {
|
||||
const errorMessages = Array.isArray(errors[fieldName])
|
||||
? errors[fieldName]
|
||||
: [errors[fieldName]];
|
||||
|
||||
// Ищем поле по имени
|
||||
const field = form.querySelector(`[name="${fieldName}"]`);
|
||||
|
||||
if (field) {
|
||||
// Добавляем класс ошибки
|
||||
field.classList.add('is-invalid');
|
||||
|
||||
// Ищем или создаём контейнер для ошибки
|
||||
let feedbackEl = field.parentElement.querySelector('.invalid-feedback');
|
||||
|
||||
if (!feedbackEl) {
|
||||
feedbackEl = document.createElement('div');
|
||||
feedbackEl.className = 'invalid-feedback';
|
||||
field.parentElement.appendChild(feedbackEl);
|
||||
}
|
||||
|
||||
// Показываем сообщение об ошибке
|
||||
feedbackEl.textContent = errorMessages.join(', ');
|
||||
feedbackEl.style.display = 'block';
|
||||
|
||||
console.log(`[EmployeeForm] Ошибка в поле ${fieldName}:`, errorMessages);
|
||||
} else {
|
||||
console.warn(`[EmployeeForm] Поле ${fieldName} не найдено в форме`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показ сообщения об успехе
|
||||
*/
|
||||
function showSuccessMessage(message) {
|
||||
// Можно использовать toast или простой alert
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alertDiv.style.zIndex = '9999';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// Автоматически убираем через 3 секунды
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показ сообщения об ошибке
|
||||
*/
|
||||
function showErrorMessage(message) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'alert alert-danger alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alertDiv.style.zIndex = '9999';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// Автоматически убираем через 5 секунд
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user