Ещё парочка способов подружить ajax и FormLister в Evo 1.4.x / 3.x

Суть: делаем отправку формы, не перезагружая вообще ничего. Этот способ максимально заботливый в плане UI.
Даже файлы, которые юзер полчаса раскидывал по инпутам, останутся на месте в случае ошибки в валидации (самое бесячее, согласитесь?)

Принцип: FormLister работает в режиме api, возвращая только результаты валидации. Делает это он в формате json. Мы принимаем, разбираем ответ, ищем поля с ошибками, помечаем их. Даём юзеру ещё шанс.
В случае корректной обработки формы, FormLister добавляет в ответ чанк, сообщающий об успехе. Достаём его, вставляем на место формы. Done.

Я постарался засунуть в форму побольше инпутов — для образца и дальнейшего копипаста. Используются Bootstrap 5 и jquery, но это не принципиально. Сгодится любой способ отправить форму и получить json.

Демо на 1.4: тут

Evolution 1.4.x

— для олдфагов и меня.

Шаблон:

<!DOCTYPE html>
<html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=[(modx_charset)]" /> 
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
</head>
<body>
    <section class="main">
        <div class="container">
	    <div class="row content">
		<div class="col-sm-8">
		    <h1>[*pagetitle*]</h1>
		    {{award_form}}
		</div>
		<aside class="col-sm-4"></aside>
	    </div>
	</div>
    </section>
    <script src="assets/templates/theme/js/ajax.js"></script>
</body>
</html>


Принципиально обязательно в этом шаблоне только воткнуть jquery и файлик ajax.js. Остальное — чисто визуальщина, чтобы вы скопировали, вставили и потестили.

Форма

(из чанка award_form)

<div id="award_form_wrapper">
    <form method="post" id="award_form"  enctype="multipart/form-data" class="needs-validation ">
	<input type="hidden" name="formid" value="award_form">
	<div class="form-group mb-3" data-field-wrapper="name">
	    <label for="name" class="form-label">Как вас зовут?</label>
	    <input class="form-control form-control-sm" id="name" name="name"  type="text" >
            <div class="invalid-feedback"></div>
	</div>
	<div class="form-group">
	    <label class="form-label" >Что хотите сообщить?</label>
	</div>
	<div class="form-group mb-3" data-field-wrapper="topic">
	    <div class="form-check form-check-inline">
		<label class="form-check-label"><input class="form-check-input" type="radio" name="topic" value="Предложение">Есть предложение</label>
	    </div>
	    <div class="form-check form-check-inline">
		<label class="form-check-label"><input class="form-check-input " type="radio" name="topic" value="Жалоба">Есть жалоба</label>
	    </div>
	    <div class="invalid-feedback"></div>
	</div>
	    <div class="form-group mb-3" data-field-wrapper="comment">
		<label for="comment"  class="form-label">Текстом:</label>
		<textarea class="form-control form-control-sm" id="comment" name="comment" ></textarea>
		<div class="invalid-feedback"></div>
        </div>
	<div class="form-group mb-3" data-field-wrapper="files">
	    <label for="files"  class="form-label">Фотки с места событий:</label>
            <input class="form-control form-control-sm" type="file" class="form-control" id="files" name="files[]" multiple>
	    <div class="invalid-feedback"></div>
	</div>
	<div class="form-group mb-3" data-field-wrapper="department">
	    <label for="department" class="form-label" >Обслуживающий офис</label>
	    <select name="department" class="form-select form-select-sm">
		<option value="1">ул. Толстых Партизан</option>
		<option value="2">ул. Добрых Строителей</option>
		<option value="3">Не помню, был пьян</option>
	    </select>
	    <div class="invalid-feedback"></div>
	</div>
	<div class="form-group " >
	    <label class="form-label" for="products">Какими услугами вы пользуетесь?</label>
	</div>
	<div class="form-group mb-3" data-field-wrapper="products">
	    <div class="form-check form-check-inline">
		<label><input class="form-check-input" checked type="checkbox" name="products[]" value="1">Техподдержка</label>
	    </div>
	    <div class="form-check form-check-inline">
		<label><input class="form-check-input"  type="checkbox" name="products[]" value="2">Хостинг</label>
	    </div>
	    <div class="form-check form-check-inline">
		<label><input  class="form-check-input" type="checkbox" name="products[]" value="3">Разработка</label>
	    </div>
	    <div class="invalid-feedback"></div>
	</div>
	<div class="form-group mb-3" data-field-wrapper="agree">
	    <div class="form-check">
		<label><input class="form-check-input" type="checkbox" name="agree" value="Да">Я согласен с правилами обработки обращений</label>
	    </div>
	    <div class="invalid-feedback"></div>
	</div>
	<div class="form-group field">
	    <input type="submit" value="Отправить" class="btn btn-primary">
	</div>
    </form>
</div>


Обратите внимание на айди обёртки формы (award_form_wrapper) и айди формы (award_form). Эти переменные вы можете изменять, как вам угодно. При этом не забудьте также поменять их включения в файлах ajax.js и ajax.php ниже.

А вот дата-атрибуты типа data-field-wrapper=«name» нуждаются в вашем внимании. В значение data-field-wrapper вы должны подставить имя того поля, которое у вас будет участвовать в валидации. Именно по этому атрибуту js отработает в итоге и подсветит ошибки. При этом мультиполя с именами типа files[] ставьте просто как files, без скобок.

Скрипт
assets/templates/theme/js/ajax.js

var ajax = {
    sendForm: function(form_wrapper_id,form_id,query_key){
	//про аргументы дальше
	$.ajax({
	    type: 'post',
	    url: '/ajax.php?q=' + query_key,
	    data: new FormData($(form_id)[0]),
	    cache: false,
	    dataType: "json",
	    contentType: false,
	    processData: false,
	    success: function(data) {
                $(form_id).find('[data-field-wrapper]').removeClass('has-error');
                $(form_id).find('[data-field-wrapper]').find('.invalid-feedback').html('');
                if (data.status == false) {
                    $.each(data.errors, function(index, item) {
                        var error_text = '';
                        var validate_errors_types = Object.keys(item);
                        for (var key in validate_errors_types) {
                            var error_text = item[validate_errors_types[key]];
                        }
                        var field_container = $(form_id).find('[data-field-wrapper="' + index + '"]');
                        field_container.find('.invalid-feedback').addClass('d-block');
                        field_container.addClass('has-error');
                        field_container.find('.invalid-feedback').html(error_text);
                    });
                } else if (data.status == true) {
                    $(form_id).remove();
                    $(form_wrapper_id).html(data.output);
                } else {
                    //console.log('С формой ваще беда');
                }
            },
	    beforeSend: function(){
		//console.log('Запрос начат');
         $(form_id).find('input,button,select,textarea').prop("disabled",true); 
	    },
	    complete: function(data){
		//console.log('Запрос закончен');
	    $(form_id).find('input,button,select,textarea').prop("disabled",false);
	    },
	    error: function(xhr, ajaxOptions, thrownError){
		//console.log('Запрос с ошибкой');
		console.log(xhr);
	    }							
	});
    }
};


//	Вызов функции. 
//	Передаём id формы, id слоя-обёртки, значение q из запроса
//	Здесь ajax.php?q=award_form значит передаём award_form
$(document).ready(function(){
    $(document).on('submit', '#award_form',function(e){
	ajax.sendForm(
	    '#award_form_wrapper',
	    '#award_form',
	    'award_form',
	    );
	e.preventDefault();
    });
});

Никаких вызовов FormLister в самом шаблоне не происходит. А происходит только обработка формы, сборка данных и отправка всего великолепия на адрес ajax.php?q=айди_формы. В нашем случае это award_form.

ajax.php
А тут уже и магия внутри

<?php
define('MODX_API_MODE', true);
include_once("index.php");
$modx->db->connect();
if (empty($modx->config)) {
	$modx->getSettings();
}
switch ($_REQUEST['q']) {
	case 'award_form':	//  тот самый award_form из функции js выше
		$result = $modx->runSnippet('FormLister', array(
			'formid' => 'award_form',	// Айди формы
			'api' => 2,
			'rules' => [
				"name" => [
					"required" => "Введите имя"
				],
				"comment" => [
					"required" => "Текст не может быть пустым",
					"minLength" => [
						"params" => 10,
						"message" => "Не менее 10 символов"
					]
				],
				"products" => [
					"required" => "Нужно выбрать 2 услуги",
					"minCount" => [
						"params" => 2,
						"message" => "Минимум 2 услуги"
					]
				],
				"department" => [
					"required" => "Выберите офис"
				],
				"topic" => [
					"required" => "Выберите тему"
				],
				"agree" => [
					"required" => "Вы не можете отправить обращение, если не согласны с правилами"
				]
			],
			'fileRules' => [
				"files" => [
					"required" => "Приложите от 2 до 5 фото",
					"maxSize" => [
						"params" => 2048,
						"message" => "Фото не более 2 Мб"
					],
					"allowed" => [
						"params" => [["jpg", "jpeg", "png"]],
						"message" => "Только фото"
					],
					"maxCount" => [
						"params" => 5,
						"message" => "Не больше 5 фото"
					],
					"minCount" => [
						"params" => 2,
						"message" => "Не меньше 2 фото"
					]
				]
			],
			'attachments' => 'files',
			'formControls' => 'agree,topic,department,products',
			'to' => 'your@email.to',
			'subject' => 'Заявка с сайта',
			'successTpl' => 'successTplFormlisterJson',
			'reportTpl' => 'reportTplFormlisterJson'
		));
		echo $modx->parseDocumentSource($result);
		break;
}


В чанке successTplFormlisterJson хранится ваше «Спасибо за письмо, наши менеджеры уже...» а в reportTplFormlisterJson файл самого письма. Там просто ваши поля в формате [+name.value+] и т.д.

Evolution 3.x

— для развиваться ©

Преамбула:
Если у вас не поставлен FormLister в Evolution 3.x., нужно:
0. установить его из Extras
1. зайти в core/custom/composer.json
2. Найти там секцию require. Сунуть туда «pathologic/modxapi»: "*"
3. открыть консоль в папке core, и выполнить команду composer update

Шаблон
Создаём в админке шаблон с псевдонимом clear_ajaxform
Создаём файл этого шаблона /views/clear_ajaxform.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=[(modx_charset)]" /> 
    <title>{{ $documentObject['pagetitle']}}</title>
    <base href="{{ $modx->getConfig('site_url') }}">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"  crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
</head>
<body>
    <section class="main">
        <div class="container">
	    <div class="row content">
	        <div class="col-sm-8">
		    <h1>{{ $documentObject['pagetitle']}}</h1>
		    @include('parts.form')
		</div>
		<aside class="col-sm-4">
		    <p>Всякое с аяксами</p>
		</aside>
	    </div>
	</div>
    </section>
    <script src="theme/js/ajax.js"></script>
</body>
</html>


Файл формы
Создаём файл /views/parts/form.blade.php

<div id="award_form_wrapper">
    <form method="post" id="award_form"  enctype="multipart/form-data" class="needs-validation ">
        <input type="hidden" name="formid" value="award_form">
        <div class="form-group mb-3" data-field-wrapper="name">
            <label for="name" class="form-label">Как вас зовут?</label>
            <input class="form-control form-control-sm" id="name" name="name"  type="text" >
	    <div class="invalid-feedback"></div>
        </div>
        <div class="form-group">
            <label class="form-label" >Что хотите сообщить?</label>
        </div>
        <div class="form-group mb-3" data-field-wrapper="topic">
            <div class="form-check form-check-inline">
                <label class="form-check-label"><input class="form-check-input" type="radio" name="topic" value="Предложение">Есть предложение</label>
	    </div>
	    <div class="form-check form-check-inline">
	        <label class="form-check-label"><input class="form-check-input " type="radio" name="topic" value="Жалоба">Есть жалоба</label>
	    </div>
	    <div class="invalid-feedback"></div>
        </div>
	<div class="form-group mb-3" data-field-wrapper="comment">
            <label for="comment"  class="form-label">Текстом:</label>
            <textarea class="form-control form-control-sm" id="comment" name="comment" ></textarea>
            <div class="invalid-feedback"></div>
        </div>
        <div class="form-group mb-3" data-field-wrapper="files">
            <label for="files"  class="form-label">Фотки с места событий:</label>
            <input class="form-control form-control-sm" type="file" class="form-control" id="files" name="files[]" multiple>
            <div class="invalid-feedback"></div>
        </div>
        <div class="form-group mb-3" data-field-wrapper="department">
	    <label for="department" class="form-label" >Обслуживающий офис</label>
	    <select name="department" class="form-select form-select-sm">
	        <option value="1">ул. Толстых Партизан</option>
	        <option value="2">ул. Добрых Строителей</option>
	        <option value="3">Не помню, был пьян</option>
	    </select>
	    <div class="invalid-feedback"></div>
	</div>
        <div class="form-group " >
            <label class="form-label" for="products">Какими услугами вы пользуетесь?</label>
        </div>
        <div class="form-group mb-3" data-field-wrapper="products">
            <div class="form-check form-check-inline">
                <label><input class="form-check-input" checked type="checkbox" name="products[]" value="1">Техподдержка</label>
            </div>
            <div class="form-check form-check-inline">
                <label><input class="form-check-input"  type="checkbox" name="products[]" value="2">Хостинг</label>
            </div>
            <div class="form-check form-check-inline">
                <label><input  class="form-check-input" type="checkbox" name="products[]" value="3">Разработка</label>
            </div>
            <div class="invalid-feedback"></div>
        </div>
        <div class="form-group mb-3" data-field-wrapper="agree">
            <div class="form-check">
                <label><input class="form-check-input" type="checkbox" name="agree" value="Да">Я согласен с правилами обработки обращений</label>
            </div>
            <div class="invalid-feedback"></div>
        </div>
        <div class="form-group field">
	    <input type="submit" value="Отправить" class="btn btn-primary">
        </div>
    </form>
</div>


Абсолютно та же самая форма.

ajax.js
Такой же, как и для 1.4. Разве что я размещаю это в папке /theme/js/ajax.js

Route
Лезем в роуты core/custom/routes.php

Добавляем там вот такое
Route::post('/ajax/{action}', [AjaxController::class, 'index']);


И вверху
use Illuminate\Support\Facades\Route;
use EvolutionCMS\Main\Controllers\AjaxController;


Первая строка, скорее всего, у вас уже есть. А во второй мы подключаем свой контроллер для аякса.

Создаём контроллер AjaxController либо же используем свой, но тогда правьте роуты под свои нужды. Тут полная свобода воли, и вполне можно было обойтись без case в контроллере, просто сразу вызвав нужный метод вместо index.

Путь
/core/custom/packages/main/src/Controllers/AjaxController.php


И пишем, в принципе, всё то же, что и в Evo 1.4, но с поправками на развиваться-стайл.

<?php
namespace EvolutionCMS\Main\Controllers;
class AjaxController extends BaseController
{
    public function index($action){
        switch ($action) {
            case 'award_form':
                $result = $this->evo->runSnippet('FormLister', array(
                    'formid' => 'award_form',
                    'api' => 2,
                    'rules' => [
                        "name" => [
                            "required" => "Введите имя"
                        ],
                        "comment" => [
                            "required" => "Текст не может быть пустым",
                            "minLength" => [
                                "params" => 10,
                                "message" => "Не менее 10 символов"
                            ]
                        ],
                        "products" => [
                            "required" => "Нужно выбрать 2 услуги",
                            "minCount" => [
                                "params" => 2,
                                "message" => "Минимум 2 услуги"
                            ]
                        ],
                        "department" => [
                            "required" => "Выберите офис"
                        ],
                        "topic" => [
                            "required" => "Выберите тему"
                        ],
                        "agree" => [
                            "required" => "Вы не можете отправить обращение, если не согласны с правилами"
                        ]
                    ],
                    'fileRules' => [
                        "files" => [
                            "required" => "Приложите от 2 до 5 фото",
                            "maxSize" => [
                                "params" => 2048,
                                "message" => "Фото не более 2 Мб"
                            ],
                            "allowed" => [
                                "params" => [ ["jpg","jpeg","png"] ],
                                "message" => "Только фото"
                            ],
                            "maxCount" => [
                                "params" => 5,
                                "message" => "Не больше 5 фото"
                            ],
                            "minCount" => [
                                "params" => 2,
                                "message" => "Не меньше 2 фото"
                            ]
                        ]
                    ],
                    'attachments' => 'files',
                    'formControls' => 'agree,topic,department,products',
                    'to' => 'email@email.to',
                    'reportTpl' => '@B_FILE: parts/form_report',
                    'subject' => 'Заявка с сайта',
                    'successTpl' => '@B_FILE: parts/form_thanks',
                ));
                return $result;
                
                break;
            default:
                
                break;
        }
    }
}


Как вы можете заметить, есть парочка отличий.

@B_FILE: parts/form_report - файл письма
@B_FILE: parts/form_thanks - файл "спасиба".


Оба файла лежат в папке /views/parts/
Оформляйте их уже на свой вкус.

P.S.:
Если вы модифицируете скрипт и будете, скажем, создавать ресурсы, а не отправлять почту, в вашем AjaxController нужно будет заюзать соответствующий контроллер. Типа

use Pathologic\EvolutionCMS\MODxAPI\modResource;

А в вызове уже подставлять котзнаетчто что-то типа
'controller' => 'Content'
'model' => 'Pathologic\EvolutionCMS\MODxAPI\modResource'
'userModel'=> 'Pathologic\EvolutionCMS\MODxAPI\modUsers'

Загляните в core/vendor/pathologic/modxapi/src

1 комментарий

avatar
Привет,
решил написать тут так как проблема связанно как раз с отправкой формы аяксом.

У меня есть старый проект на 1.4.x где форма работает через плагин и все работает нормально. Новый проект решил постаивть на Evolution 3.x и использовать старых подход через плагин evoAjax сделал по аналогии, но не использовал jQuery.ajax() а использовал fetch().
И никак не могу заставить заработать.
Ошибка в консоли:
<code>domain.loc/contactajaxform 404 (Not Found)</code>

JS:

<code>const form = document.getElementById("contactForm");
if (form) {
	form.addEventListener("submit", handleSubmit)
}
async function handleSubmit(event) {
	event.preventDefault();

	const dataForm = new FormData(event.target);

	fetch(event.target.action, {
		method: form.method,
		body: dataForm
	})
	.then(response => {
		console.log("response", response);
		response.json();
	})
	.then(result => {
		console.log("result", result);
	})
	.catch(error => {
		console.log("error", error);
	});

}</code>

Шаблон формы:

<code><form id="contactForm" method="post" action="[(site_url)]contactajaxform">
    <input type="hidden" name="formid" value="contactForm">
    <div class="alert d-none" id="contactMessage">[+form.messages+]</div>

    <div class="form-row">
        <div class="form-col form-col-half">
            <div class="form-group">
                <label for="c_name">Full name *</label>
                <input type="text" class="form-control [+c_name.errorClass+][+c_name.requiredClass+]" name="c_name" id="c_name" value="[+c_name.value+]">
                [+c_name.error+]
            </div>
        </div>
        <div class="form-col form-col-half">
            <div class="form-group">
                <label for="c_email">Email *</label>
                <input type="email" class="form-control [+c_email.errorClass+][+c_email.requiredClass+]" name="c_email" id="c_email" value="[+c_email.value+]">
                [+c_email.error+]
            </div>
        </div>
        <div class="form-col form-col-full">
            <div class="form-group">
                <label for="c_message">Message *</label>
                <textarea class="form-control [+c_message.errorClass+][+c_message.requiredClass+]" rows="4" name="c_message" id="c_message">[+c_message.value+]</textarea>
                [+c_message.error+]
            </div>
        </div>
        <div class="form-col form-col-full form-col-submit text-center text-lg-end">
            <button type="submit" class="btn btn-outline-dark btn-hover-green btn-lg fw-bold btn-mobile-wide" id="contactFormSubmit">Send message</button>
        </div>
    </div>
</form></code>

evoAjax плагин (событие на OnPageNotFound)

<code><?php
if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest') {
    return;
}

switch($_GET['q']){
	case 'contactajaxform':
		echo $modx->runSnippet('FormLister', array(
			'debug'=>'0',
			'formid' => 'contactForm',
			'prepareProcess'=>'mailsaverFormLister',
			'to' => 'test@test.com',
			'from' => 'test@test.com',
			'parseMailerParams'=> '1',
			'replyTo'=> '[+c_email.value+]',
			'subjectTpl' => '@CODE: Contact form message: [+c_subject.value+], from: ' . $modx->config['site_name'] ,
			'errorClass'=> ' is-invalid',
			'requiredClass'=> ' is-invalid',
			'rules'=> '{
					"c_name":{
						"required":"Enter your name"
					},
					"c_email":{
						"required":"Enter your email",
						"email":"Email is not valid"
					},
					"c_message":{
						"required":"Enter your message"
					}
				}',
			'submitLimit'=> '10',
			'protectSubmit'=> '0',
			'lang' => 'english',
			'formControls'=>'subject',
			'messagesOuterTpl'=>'@CODE:<div class="alert alert-danger" role="alert">[+messages+]</div>',
			'errorTpl'=>'@CODE: <div class="invalid-feedback">[+message+]</div>',
			'successTpl'=> 'ContactFormSuccess',
			'formTpl' => 'ContactForm', 
			'reportTpl'=>'ContactFormReport',
			'removeEmptyPlaceholders' =>'1',
			'prepare'=> 'checkSpamTimeFL'
		));
		die();
		break;
}</code>

Подскажите в чем может быть проблема? Такое ощущение что плагин вообще не срабатывет.
Комментарий отредактирован 2022-03-01 17:32:49 пользователем SerNeo
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.