Хватить это терпеть! Или зачем столько чанков в FormIt!?

Привет сообществу!

Небольшая хитрость которая поможет новичкам избавиться от множества чанков/файлов писем с сайта и реализовать их в одном чанке/файле. Кроме этого пару слов о том, что происходит с MODX в конце заметки.

Предисловие:

Пару месяцев назад в чате был небольшой джентльменский спор между Павлом Зарубином и Евгением на тему «Так ли хорош FormIt?». В этом споре, я разместился где-то посередине так как я вижу и плюсы и минусы данного компонента. Эта заметка об одном из минусов, ну и конечно же решение.

Итак, проблема

Много чанков


А если быть точнее, столько же, сколько и различных форм на сайте. Когда ко мне попадают чужие проекты, я вижу там 100500 чанков которые отличаются только тем, что у них разное количество полей или просто разные поля.

Они выглядят примерно так:

...
<li>Имя: [[+name]]</li>
<li>Телефон: [[+phone]]</li>
...

...
<li>Имя: [[+name]]</li>
<li>Email: [[+email]]</li>
...

...
<li>Имя: [[+name]]</li>
<li>Email: [[+email]]</li>
<li>Тема обращения: [[+subject]]</li>
<li>Сообщение: [[+message]]</li>
...
и так далее.

Хоть я и в бэкенде недавно, но меня это удручает, поскольку сегодня существует понятие «Наследование» и например в miniShop2 те же самые письма реализованы прекрасно с помощью fenom.

Решение


Предлагаю самое простое решение, воспользоваться хуком и немного схитрить используя свойство fieldNames сниппета FormIt который служит тому чтобы при записи данных форм в БД с помощью родного хука FormItSaveForm переименовывать названия полей, ведь по умолчанию будут использованы ключи глобального массива с формой. Синтаксис такой: Название поля==Новое название и это всё через запятую. Название поля — это атрибут name у полей (Вдруг кто не знал).

1. Вызов FormIt

// Синтаксис MODX

[[FormIt?
    ...
    &hooks=`fields,email`    // сниппет fields мы с вами создадим в следующем шаге
    &fieldNames=`name==Имя,phone==Контактный телефон`
    &emailTpl=`email.tpl`
]]

// Синтаксис fenom

{'FormIt' | snippet : [
    ...
    'hooks' => 'fields,email',    // сниппет fields мы с вами создадим в следующем шаге
    'fieldNames' => 'name==Имя,phone==Контактный телефон',
    'emailTpl' => '@FILE chunks/email/email.tpl'
]}

2. Создание сниппета/хука
Создадим сниппет и укажем fields в качестве названия с таким кодом:

<?php
$fields = explode(',', $hook->formit->config['fieldNames']);

foreach ($fields as $key => $field) {
    $f = explode('==', trim($field));
    $value = $hook->getValue($f[0]);
    
    if (is_array($value)) {
        $value = implode(', ', $value);
    }
    
    $result[] = array(
        'name' => $f[1],
        'value' => $value,
    );
}

$hook->setValue('fields', $modx->toJSON($result));

return true;

«Что этот сниппет себе позволяет?!» или что сниппет/хук делает объясню вкратце:

Забирает значение свойства fieldNames и указанных данных пользователя заполнившего форму и с помощью магии методов создает новое поле fields, т.е. в чанке письма будет доступен плейсхолдер
[[+fields]] в котором будет JSON массив с названием и значением от пользователя.

3. Чанк/файл email.tpl

Я лично из лагеря fenom, но стараюсь и ради тех кто всё еще пользуется родным синтаксисом
Если вы не пользуйтесь fenom, то весьма кстати вам пригодиться сниппет getImageList который идёт вместе с MIGX.

Кстати, у меня есть пара заметок о MIGX где я подробно рассказывал о нём. Вот первая заметка, а вот вторая, почитайте, уверен, будет интересно, если еще не читали.

// Синтаксис MODX


<h3>Обратная связь:</h3>

[[getImageList?
    &value=`[[+fields]]`
    &tpl=`@CODE:<li>[[+name]]: [[+value]]</li>`
]]


// Синтаксис fenom

<h3>Обратная связь:</h3>

{foreach $fields | fromJSON as $field}
    <li>{$field['name']}: {$field['value']}</li>
{/foreach}

Итог

Теперь в вызове сниппета FormIt указывайте в свойство fieldNames список нужных полей и пользуйтесь одним чанком.

__________________________________



Пару слов о самом MODX

К данной заметке это не относится, но хочется выразится о MODX и о MODX3

Недавно Иван Бочкарёв писал о том, что происходит с MODX на Github и мне также хочется призвать разработчиков к активности. Я считаю, что MODX переживает не лучшие времена и понятно, что его развитие в основном зависит от более опытных разработчиков и от MODX LLC, но мы ведь тоже можем хоть немного повлиять на развитие нашей платформы. Я также в свою очередь делаю вклад насколько мне позволяют знания, опыт, время и возможности. Вступайте в группу MODX Contributors

Вам спасибо за внимание, а мне потому что…

☕ Угостить чашкой кофе

UPD

Обновлен сниппет для значений radio

UPD: 14.11.2019

Обновлен сниппет/хук, добавлен trim
Баха Волков
11 февраля 2019, 14:56
modx.pro
31
4 722
+24

Комментарии: 21

brioni
11 февраля 2019, 15:04
+1
Хорошее решение, а вот ссылки на телеграмм уже почти год не работают. Не устаю повторять, что для телеги конверсия ссылок в формате: @groupname гораздо выше
    Иван Бочкарев
    11 февраля 2019, 15:18
    0
    Справедливое замечание
    Я поменял тип группы: t.me/modx_contributors
      Баха Волков
      11 февраля 2019, 15:22
      0
      Хорошее решение
      Спасибо

      а вот ссылки на телеграмм уже почти год не работают. Не устаю повторять, что для телеги конверсия ссылок в формате: @groupname гораздо выше
      Поменял в заметке
      mngatoff
      11 февраля 2019, 15:06
      0
      не знал, что формит умеет в 'emailTpl' => '@FILE chunks/email/email.tpl'
      или я где-то пропустил подключение pdoTools?
      я использовую formit через ajaxform, и у меня не получалось в чанках письма юзать fenom — только родной синтаксис и тот без модификаторов. то есть если значение [[+phone]] пустое, то в письме так и будет — "[[+phone]]"
        Баха Волков
        11 февраля 2019, 15:20
        0
        не знал, что формит умеет в 'emailTpl' => '@FILE chunks/email/email.tpl'
        или я где-то пропустил подключение pdoTools?
        В версии 3.0.0 появилась:

        • Added default file-based chunks as objects
        • Added support for pdoTools getChunk method
          mngatoff
          11 февраля 2019, 15:22
          0
          да, надо читать чейндж-логи))
          Денис Чубенко
          12 февраля 2019, 22:32
          +3
          Вот такой вариант, работает
          <table>
              [[+name:notempty=`<tr>
                  <td style="padding:2px 7px 2px 0px"><b>Имя:</b></td>
                  <td>[[+name]]</td>
              </tr>`]]
              [[+phone:notempty=`<tr>
                  <td style="padding:2px 7px 2px 0px"><b>Телефон:</b></td>
                  <td>[[+phone]]</td>
              </tr>`]]
              [[+email:notempty=`<tr>
                  <td style="padding:2px 7px 2px 0px"><b>Почта:</b></td>
                  <td>[[+email]]</td>
              </tr>`]]
              [[+link:notempty=`<tr>
                  <td style="padding:2px 7px 2px 0px"><b>Ссылка:</b></td>
                  <td>[[+link]]</td>
              </tr>`]]
              [[+mess:notempty=`<tr>
                  <td style="padding:2px 7px 2px 0px"><b>Сообщение:</b></td>
                  <td>[[+mess]]</td>
              </tr>`]]
              <tr>
                  <td style="padding:2px 7px 2px 0px"><b>Страница:</b></td>
                  <td><a href="[[++site_url]][[+uri]]">[[+page]]</a></td>
              </tr>
          </table>
          Добавляю все поля которые мне нужны из разных форм. Клиенту приходят только те поля, что заполнены.
          Николай
          11 февраля 2019, 16:04
          4
          +1
          Выложу свой способ управления множеством форм для сниппета ajaxForm, может пригодится.

          Создаём чанк ajaxForms:

          {if !$form} {set $form = ''}{/if}
          {if !$hooks} {set $hooks = 'email,FormItSaveForm'}{/if}
          {if !$emailSubject} {set $emailSubject = 'Тема письма'}{/if}
          {if !$emailTo} {set $emailTo = $_modx->config['callback_email']}{/if}
          {if !$validationErrorMessage} {set $validationErrorMessage = 'В форме содержатся ошибки!'}{/if}
          {if !$successMessage} {set $successMessage = '<div class="name">Спасибо</div><p>Ваше сообщение успешно отправлено</p>'}{/if}
          {if !$vTextMaxLength} {set $vTextMaxLength = '<div>Проверьте правильность заполнения</div>'}{/if}
          {if !$vTextMinLength} {set $vTextMinLength = '<div>Проверьте правильность заполнения</div>'}{/if}
          {if !$vTextRequired} {set $vTextRequired = 'Это поле обязательно для заполнения'}{/if}
          {if !$validate} {set $validate = 'name:required'}{/if}
          {if !$formFields} {set $formFields = 'name,pageId,pagetitle,email,message,file'}{/if}
          {if !$formName} {set $formName = 'Имя формы'}{/if}
          {if !$validationErrorMessage} {set $validationErrorMessage = 'В форме содержатся ошибки!'}{/if}
          
          [[!ajaxForm?
          	&form=`{$form}`
          	&hooks=`{$hooks}`
          	&emailSubject=`{$emailSubject}`
          	&emailTo=`{$emailTo}`
          	&validationErrorMessage=`{$validationErrorMessage}`
          	&successMessage=`{$successMessage}`
          	&vTextMaxLength=`{$vTextMaxLength}`
          	&vTextMinLength=`{$vTextMinLength}`
          	&vTextRequired=`{$vTextRequired}`
          	&validate=`{$validate}`
          	&validationErrorMessage=`{$validationErrorMessage}`
          	&formName=`{$formName}`
          	&formFields=`{$formFields}`
          	&fieldNames=`{$fieldNames}
              `
          ]]

          Это единый чанк, в котором вызывается сниппет ajaxForm с необходимыми параметрами.

          В моём случае каждая отдельная форма — это отдельный чанк. Если эти чанки мало чем различаются, то можно использовать extends из fenom, переписывая лишь изменяющиеся значения, а остальное не трогать.

          И далее в шаблоне страницы вызов нужной формы:

          {include 'ajaxForms' 
              form='callback'
              emailSubject='Обратный звонок'
              validate='name:required,phone:required'
              formFields='name,phone,pagetitle'
              formName='Обратный звонок'
          }

          В нём мы подменили значения, передающиеся в чанк ajaxForm.

          Пример чанка callback с формой:

          <div style="display: none;" id="{block 'id'}callback{/block}" class="popup">
              <div class="title">{block 'title'}Заказать обратный звонок{/block}</div>
              <div class="desc">{block 'desc'}Оставьте заявку, и мы свяжемся с вами в самое ближайшее время{/block}</div>
              <form role="form" method="POST" action="[[~[[*id]]]]" enctype="multipart/form-data">
                  <input type="hidden" name="pagetitle" value="[[*pagetitle]] ([[*id]])">
                      
                  <input type="text" name="name" value="[[!+fi.name]]" placeholder="Ваше имя" required="required" />
                  <span class="error error_name">[[+fi.error.name]]</span>
                  
                  <input type="tel" name="phone" value="[[!+fi.phone]]" placeholder="Ваш телефон" required="required" />
                  <span class="error error_phone">[[+fi.error.phone]]</span>
                  
                  <div class="privacy">Отправляя заявку, Вы соглашаетесь на обработку персональных данных
          согласно <a href="{29|url}">Пользовательскому соглашению</a></div>
                  
                  <button class="button-yellow" type="submit" value="{md5(rand())}" name="submit">Отправить</button>
              </form>
          </div>

          Если нужно вызвать похожую форму но с другим заголовком и другими параметрами, то создадим чанк question, который наследует чанк callback:

          {extends 'callback'}
          
          {block 'id'}question{/block}
          {block 'title'}Задать вопрос{/block}
          {block 'desc'}Задайте свой вопрос, и мы с вами свяжемся{/block}

          И вызов этой формы в шаблоне страницы:

          {include 'ajaxForms' 
              form='question'
              emailSubject='Задать вопрос'
              validate='name:required,phone:required'
              formFields='name,phone,pagetitle'
              formName='Задать вопрос'
          }

          Таким образом, для всех форм мы можем задать единые параметры вызова сниппета ajaxForm, и при необходимости перезаписать их. А также под каждую форму либо создать отдельные чанки, либо унаследовать от одного чанка кучу других с индивидуальными параметрами.

          Дополнительно я скрываю уведомления jgrowl js-скриптом:

          $(document).on('af_complete', function(event, response) {
              if (response.success) {
                  $.fancybox.close();
          
                  $.fancybox.open({
                  	src  : '#popup-success',
                  	type : 'inline',
                  	opts : {
                  		afterShow : function( instance, current ) {
                  			// console.info( 'done!' );
                  		}
                  	}
                  });
              } else {
                  for (var prop in response.data) {}
              }
              response.message='';
          });

          Все уведомления об ошибках показываются под полями форм, в коде span c классом error. А если форма успешно отправлена, то с помощью скрипта выше мы скрываем popup-форму, если она была открыта, и показываем другое popup-окно:

          <div style="display: none;" id="popup-success" class="popup">
              <div class="title">Данные успешно отправлены</div>
              <div class="success">
                  <img src="/assets/img/success.png" alt="">
              </div>
          </div>

          Оно же всплывает и при отправке обычных не popup-форм. Да, насчёт popup-окон, актуально при использовании flexbox3.
            Баха Волков
            11 февраля 2019, 16:23
            3
            +5
            Позвольте переписать ваш чанк ajaxForms:

            {'!AjaxForm' | snippet : [
                'form' => $form,
                'hooks' => $hooks !: 'email,FormItSaveForm',
                'emailSubject' => $emailSubject !: 'Тема письма',
                'emailTo' => $emailTo !: $_modx->config.callback_email,
                'validationErrorMessage' => $validationErrorMessage !: 'В форме содержатся ошибки!',
                'successMessage' => $successMessage !: '<div class="name">Спасибо</div><p>Ваше сообщение успешно отправлено</p>',
                'vTextMaxLength' => $vTextMaxLength !: '<div>Проверьте правильность заполнения</div>',
                'vTextMinLength' => $vTextMinLength !: '<div>Проверьте правильность заполнения</div>',
                'vTextRequired' => $vTextRequired !: 'Это поле обязательно для заполнения',
                'validate' => $validate !: 'name:required',
                'formName' => $formName !: 'Имя формы',
                'formFields' => $formFields !: 'name,pageId,pagetitle,email,message,file',
            ]}

            Так ведь намного лучше
              Николай
              11 февраля 2019, 16:25
              0
              Согласен, так красивее :)
              Баха Волков
              11 февраля 2019, 16:27
              +2
              И чанк callback также:

              <div style="display: none;" id="{block 'id'}callback{/block}" class="popup">
                  <div class="title">{block 'title'}Заказать обратный звонок{/block}</div>
                  <div class="desc">{block 'desc'}Оставьте заявку, и мы свяжемся с вами в самое ближайшее время{/block}</div>
                  <form role="form" method="POST" action="{('id' | resource) | url}" enctype="multipart/form-data">
                      <input type="hidden" name="pagetitle" value="{'pagetitle' | resource} ({'id' | resource})">
                          
                      <input type="text" name="name" value="" placeholder="Ваше имя" required="required" />
                      <span class="error error_name"></span>
                      
                      <input type="tel" name="phone" value="" placeholder="Ваш телефон" required="required" />
                      <span class="error error_phone"></span>
                      
                      <div class="privacy">Отправляя заявку, Вы соглашаетесь на обработку персональных данных
              согласно <a href="{29 | url}">Пользовательскому соглашению</a></div>
                      
                      <button class="button-yellow" type="submit" value="{md5(rand())}" name="submit">Отправить</button>
                  </form>
              </div>
              Andrey Grachov
              11 февраля 2019, 17:14
              +1
              Если используется хук FormItSaveForm, то можно обойтись без лишнего сниппета:

              {foreach $_pls['savedForm.values'] | json_decode as $label => $value}
                  {$label}: {$value | e}
              {/foreach}
                Павел Гвоздь
                11 февраля 2019, 19:52
                2
                +1
                немного схитрить используя свойство fieldNames сниппета FormIt
                А разве прописав кастомный параметр при вызове FormIt (или AjaxForm) он не попадает прямиком в чанк формы?
                Я делаю примерно так:
                {'!AjaxForm' | snippet : [
                    ...
                    'formFields' => [
                        'name' => [
                            'type' => 'text',
                            'label' => '',
                            'placeholder' => 'Ваше имя',
                            'required' => true,
                        ],
                        'email' => [
                            'type' => 'email',
                            'label' => '',
                            'placeholder' => 'Ваш email',
                        ],
                        'phone' => [
                            'type' => 'text',
                            'label' => '',
                            'placeholder' => 'Контактный телефон',
                            'required' => true,
                        ],
                    ],
                    ...
                ]}

                И в чанке формы:
                {foreach $formFields as $fk => $fv}
                    {if $fv['label']?}
                        <div class="form__label-w">
                            <label class="form__label">
                                {$fv['label']}
                            </label>
                        </div>
                    {/if}
                
                    {switch $fv['type']}
                        {case 'text'}
                            <div class="form__input-w">
                                <input class="form__input" type="text" name="{$fk}" placeholder="{$fv['placeholder']}">
                            </div>
                
                        {case 'email'}
                            <div class="form__input-w">
                                <input class="form__input" type="email" name="{$fk}" placeholder="{$fv['placeholder']}">
                            </div>
                
                        {case 'textarea'}
                            <div class="form__input-w">
                                <textarea class="form__input" name="{$fk}" placeholder="{$fv['placeholder']}"></textarea>
                            </div>
                    {/switch}
                
                    <div class="form__error error_{$fk}"></div>
                {/foreach}
                  Баха Волков
                  11 февраля 2019, 20:06
                  1
                  +1
                  А разве прописав кастомный параметр при вызове FormIt (или AjaxForm) он не попадает прямиком в чанк формы?
                  Паша, попадёт конечно, отличная реализация, только в заметке речь о чанках писем или я что-то не понял?)
                    Павел Гвоздь
                    11 февраля 2019, 20:08
                    +2
                    Ой, точно! Видимо я что-то упустил)) Кстати, хорошая статья! =)
                      Баха Волков
                      11 февраля 2019, 20:23
                      +1
                      Кстати, хорошая статья! =)
                      Спасибо большое :)
                  elec3c
                  11 февраля 2019, 19:58
                  0
                  Можно модифицировать параметр &fieldNames так чтобы можно было выводить разные типы инпутов и задать обязательные параметры, возможно что-нибудь ещё

                  &fieldNames=`name:text:required==Имя, email:email:required==E-mail`
                    mngatoff
                    13 февраля 2019, 00:49
                    0
                    ребят, а кто-нибудь делал зависимую валидацию полей в formit?

                    нужно так: если поле name_1 имеет значени true, то поле name_2 становится обязательным.

                    родной параметр validate вроде как по одному полю работает, а хуки не выводят сообщение об ошибке.
                      Николай Савин
                      13 февраля 2019, 08:21
                      +1
                      хук сделать, не?
                        Павел Гвоздь
                        13 февраля 2019, 08:34
                        0
                        Разве не выводят?
                        $hook->addError('field_name', 'lexicon_key');
                        return !$hook->hasErrors();
                        Андрей Степаненко
                        05 марта 2019, 04:21
                        1
                        0
                        Для универсальных писем, с проверкой

                        Телефон: {$phone}
                        
                        {set $data = $fields | fromJSON}
                        {if count($data)}
                            <em>Текст сообщения</em>
                            <ul style="list-style: none">
                                {foreach $data as $field}
                                    <li>— <b>{$field['name']}</b>: {$field['value']}</li>
                                {/foreach}
                            </ul>
                        {/if}
                          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                          21