I'm still working on the latest version of ProcessWire (version 3.0.106) and don't have it quite ready to push to GitHub today, so we'll save that for next week. But I do have a fairly major FormBuilder release ready, and am placing it for download in the FormBuilder support board today. In this post, I'll cover what's new in this version of FormBuilder. After that, there is a how-to guide for using hooks in FormBuilder, though some might also find it also generally useful for any hooks in ProcessWire. Lastly, there's a FormBuilder hooks reference, which has been asked for a few times lately, so figured that was a good way to round out this FormBuilder blog post.

FormBuilder version 34

FormBuilder was the first ProcessWire Pro module, and it remains one of the most popular in the ProcessWire store. With each new version, FormBuilder gets more and more powerful, while remaining very simple to use. Today I'm posting version 34 (0.3.4) of FormBuilder to the support board download thread (available to FormBuilder subscribers), right after publishing this post. While I've been using this version quite a bit here (and we're using it on this site too), for the moment, I'm considering this a beta version, since there is so much that is new. So if you opt to use it, please let me know if you run into any issues. Below is a partial list of what's new in this version, along with screenshots were relevant.

New email files as attachments option. Previously files could only be accessed via protected URL on the server. Now you can optionally configure your forms to add user uploaded files as attachments to the email that gets sent to the administrator. This option is found under the Actions tab when editing a form, in the "Send email to administrators" fieldset, field: "How to handle uploaded files?"


The file attachment feature works when using the core built-in WireMail in ProcessWire 3.x. It should also work in any version of the core when paired with one of the following WireMail modules: WireMailSMTP, WireMailPHPMailer, WireMailMailgun or WireMailMandrill. As far as I can tell, WireMailSwiftMailer and WireMailMailChimp do not support attachments (though someone correct me if I'm wrong).

Support for automatically deleting form submissions (entries) from the server, after a specific number of days. This is useful if you don't want user-submitted data to live on the server indefinitely. The option can be found on the Actions tab when editing a form, in the "Save to entries database" fieldset, field: "Automatically delete entries after how many days?". FormBuilder performs entry maintenance twice per day via LazyCron.

Multi-language options are now available for Select, Checkboxes, Radios and AsmSelect Inputfields, as well as any others that extend them. Previously you had to use Page fields for multi-language options. Note however that in order to take advantage of it, you have to use ProcessWire 3.0.105 or newer.

Improved drop-down menus in Setup > Forms, now lets you drill down directly to form-specific entries or editor. In addition, the main form list (and dropdown) now shows when the last form entry was received for each form.

Support for form-specific custom email template files. While this support was actually added in the previous version, it was not yet documented. Documentation can now be found in those template files.

New markdown and HTML options for defining your success message in the form editor. Previously the success message was plain text, unless you used a hook to modify it.

New option to specify multiple auto-responder fields (fields containing email address to send auto-responder to).

Added ability to edit the tags that FormBuilder uses for embed methods A and B. It is editable in the FormBuilder module configuration “Output” fieldset. In addition, this enables you to do things like force it to always load forms from HTTPS, or use URL without scheme/host if you prefer it.

Several new hooks have been added to the form rendering, processing and saving events. We'll outline all the available hooks in this post.

In addition to the above, various improvements have been made throughout to improve appearance and UI in AdminThemeUikit. Visual improvements have been made to the FormBuilder module configuration screen. And of course this version also contains several other minor tweaks, fixes and adjustments not mentioned here.


FormBuilder hooks: how-to guide

In the FormBuilder support board, the most common questions are about solving highly custom needs with FormBuilder hooks. Several current and prospective users have asked for a guide and reference to using FormBuilder hooks, to see what kind of things are possible. Though the following is a pretty good guide to using hooks whether in FormBuilder or elsewhere in ProcessWire.

Before we jump into hooks (a development task), I should say that FormBuilder is a tool that was created to simplify creation of forms by making it possible to do so without any development. And this remains the case today. For most, you can do everthing you need to in FormBuilder without touching code or learning about hooks. FormBuilder is a tool that even your clients (who might know nothing about web development) can easily create and publish forms in. However, I've also found that many in our audience really like solving highly custom needs with FormBuilder too... modifying the way things work, using FormBuilder to solve unique problems, and doing so with elegant solutions. And admittedly, when you get into it, it can be quite fun. This guide is for those that want to open the hood and customize the way that FormBuilder works from the development side.

Where to place hooks

Code for your FormBuilder hooks (and generally most custom ProcessWire hooks) is best put in your /site/ready.php file. Though if you only need your hook code to apply in the ProcessWire admin, then you might want to place it in /site/templates/admin.php instead. Or, if you only need your hook code to apply on the front-end of your site, then you might place it in /site/templates/_init.php. But when it doubt, use /site/ready.php, because that will work for just about any instance. FormBuilder also has a /site/templates/form-builder.inc file that used to be a common place for FormBuilder hooks. I say "used to be" because after the ready.php support was added to the core a few years back, that largely replaced the need for FormBuilder to have its own hooks file.

Hook examples

First, here's a simple example of how to hook a FormBuilder method. Most of the hooks in FormBuilder come from the FormBuilderProcessor class, which is what renders and processes form submissions. All of our examples will focus on that.

$forms->addHook('FormBuilderProcessor::methodName', function($e) {
  // hook code
}); 

The $forms variable is one provided by FormBuilder, and it represents the entire FormBuilder module. You can attach your hook with “addHook”, “addHookBefore” or “addHookAfter”, depending on whether you want to execute your code before or after the method you are hooking into. Hooking before enables you to manipulate arguments that get sent to the method, while hooking after enables you to examine or manipulate the value returned from the hooked method. For cases where it doesn't matter whether your code is executed before or after the method, you can just use “addHook” (without before or after), like in the example above.

Lets take a look at hooking before a method. Hooking before the renderReady method would enable you to perform some action or modification right before the form is rendered:

$forms->addHookBefore('FormBuilderProcessor::renderReady', function($e) {
  $processor = $e->object; // FormBuilderProcessor instance
  $form = $e->arguments(0); // retrieve argument by number (zero based)
  $form = $e->arguments('form'); // OR retrieve argument by name
  if($processor->formName == 'foo') {
    // do something for form named "foo"
  } else if($processor->formName == 'bar-baz') {
    // do something else for form named "bar-baz"
  }
}); 

As an example, you might use a renderReady method to pre-populate values for a form. In this case, if a user is logged in, we'll pre-populate their email address in the appropriate field named "email":

$forms->addHookBefore('FormBuilderProcessor::renderReady', function($e) {
  $form = $e->arguments(0);
  $user = $e->wire('user');
  // if not logged in, exit now
  if(!$user->isLoggedIn()) return;
  // see if there a field in the form named 'email'
  $inputfield = $form->getChildByName('email');
  // if there is an email field and it's empty, populate user's email
  if($inputfield && $inputfield->isEmpty()) {
    $inputfield->attr('value', $user->email);
  }
}); 

Hooking after a method is useful because it lets you examine or modify its return value. In this case, a "Hello World" paragraph would appear below every form rendered by FormBuilder:

$forms->addHookAfter('FormBuilderProcessor::render', function($e) {
  $str = $e->return;
  $str .= "<p>Hello World</p>";
  $e->return = $str;
}); 

Hooking before a method is useful because it lets you not just see, but also modify its arguments before the method you are hooking gets called:

$forms->addHookBefore('FormBuilderProcessor::saveEntry', function($e) {
  $data = $e->arguments(0);
  // set field "foo" to value "bar" before it gets saved as an entry
  $data['foo'] = 'bar';
  // populate the argument back so it reflects the new value
  $e->arguments(0, $data);
}); 

Here's another example: adding a file attachment to an auto-responder email. In this case, a user subscribes to a list and receives a bonus article PDF file in their confirmation email:

$forms->addHook('FormBuilderProcessor::emailFormResponderReady', 
  function($e) {
    $form = $e->arguments(0); // or arguments('form')
    $mailer = $e->arguments(1); // or arguments('email')
    if($form->name == 'subscribe') {
      $mailer->addFileAttachment('/path/to/bonus-article.pdf');
    }
  }
); 

For this last example, lets refuse subscriptions from @hotmail.com since they've all been spam accounts lately:

$forms->addHook('FormBuilderProcessor::processInputDone', function($e) {
  $form = $e->arguments(0);
  if($form->name != 'subscribe') return; // we only want "subscribe" form
  $field = $form->getChildByName('email');
  $email = $field->attr('value');
  if(stripos($email, '@hotmail.com')) {
    $field->error('Please use a non-hotmail email address');
  }
});

When the user submits the form with a name@hotmail.com email address, the form will be rendered again with an error on the email field asking them to “Please use a non-hotmail email address.”

FormBuilder hooks reference

With the above examples aside, lets take a closer look at all the methods you can hook in the FormBuilderProcessor class. These methods enable you to hook into just about every aspect of runtime form building, rendering, processing, validation and whatever happens after a successful form submission (aka actions).

These hooks are in the format: returnType methodName($arg0, $arg1, $arg2, etc.). If there is no return value, then returnType will not be shown.

InputfieldForm populate(array $data, $entryID)
Populate $this->form with the [name=>value] data from the given associative array.

string render($id = 0)
Render the form output, or follow-up success message. If $id is populated, it is the id of existing form entry.

string renderReady(InputfieldForm $form, $formFile = '', $vars = array())
Called when ready to render, and returns rendered output. Note the $formFile and $vars arguments are only populated in embed method D.

bool processInput($id = 0)
Process input for submitted form. If $id is populated, it is the id of existing form entry.

processInputReady(InputfieldForm $form)
Called right before $form->processInput() is called.

processInputDone(InputfieldForm $form)
Called after $form->processInput() and spam filtering is completed.

int|bool saveForm(InputfieldForm $form, $id = 0)
Save the form to the database entry, page, or email(s) per form action settings. If $id is populated, it is the id of an existing entry being saved.

int saveEntry(array $data)
Save a form entry where $data is the given entry. Existing entry should have populated id property. Returns id of saved entry.

Page|null savePage(array $data, …)
Save given entry $data to a Page. There are more arguments ($status and $onlyFields) but they usually aren't present. See FormBuilder for additional arguments.

bool savePageCheckName(Page $page)
Hook called before $page->save() to validate that page name is allowed. Returns false if save should be aborted.

bool allowSavePageField(Page $page, $pageFieldName, …)
Hook called to determine if the given field info is allowed to be saved in Page? See FormBuilder for additional arguments.

savePageReady(Page $page, array $data)
Hook called right before Page is about to be saved.

array savePageDone(Page $page, array $data, $isNew, …)
Hook called after a page has been saved. Returns the entry $data that was saved. See FormBuilder for full arguments.

bool emailForm(InputfieldForm $form, array $data)
Called to email the form result to the administrator(s). Returns true on success, false on fail.

emailFormReady(InputfieldForm $form, FormBuilderEmail $email)
Called when $email object is ready, but message not yet sent.

bool emailFormResponder(InputfieldForm $form, array $data)
Called to send auto-responder email. Returns true on success, false on fail.

emailFormResponderReady(InputfieldForm $form, FormBuilderEmail $email)
Called when $email object ready, but message not yet sent. You might hook this to add a file attachment, for example.

bool postAction2(array $data)
Called to send $data to external 3rd party URL specified in the form settings. This is what gets used when you want to silently send data along to another 3rd party service like MailChimp, Salesforce, or the like. The next hook method (postAction2Ready) might actually be the more useful one though.

string postAction2Ready(WireHttp $http, array $data, $method, $url, …)
Called when ready to send to external URL. Returns response string or boolean false on fail. See FormBuilder for additional arguments.

void formSubmitSuccess(InputfieldForm $form)
Called when form has been successfully submitted and saved.

void formSubmitError(InputfieldForm $form, array $errors)
Called when there were errors that prevented successful submission of form.

string renderSuccess($message)
Called to render the given success message or process success action string (which might also instruct it to do a redirect).

string renderSuccessMessage($message, $markupTemplate = '')
Render succcess message string only (called by renderSuccess).

string renderSuccessRedirect($url)
Render or execute a redirect to given $url (called by renderSuccess).

string renderErrors()
Render error messages.

array renderErrorsReady(array $errors)
Called when errors ready to render, hooks can optionally modify $event->return array of errors.

string renderError($error, $errorTemplate = '')
Render single error message into markup.

string wrapOutput($out)
Wraps all FormBuilder output in a FormBuilder-specific <div>.

FormBuilder subscribers can download FormBuilder v34 in the FormBuilder support board (download thread, requires login). Check back here next week for ProcessWire 3.0.106 and be sure to check in at weekly.pw this weekend for the latest issue of ProcessWire Weekly. Thanks for reading and have a great weekend!


Comments

  • HMCB

    HMCB 1 month ago 20

    Thank you Ryan. As a designer, I’ve always wanted to dive into things beyond just building pages and doing more programmatic stuff. This post has just enough to get me started. Thanks again!

  • Peter Knight

    Peter Knight 1 month ago 40

    Thanks for the “Support for automatically deleting form submissions...”
    I was just showing a client how to log in and delete submissions. This will save her the trouble and help her stay GDPR compliant.

  • Sergio

    Sergio 4 weeks ago 00

    Thanks Ryan. It would be great to also add to the FormBuilder backend a field, maybe called "notes", intended for admins and editorial people so that them could write and keep important information related to each form e.g. advanced custom settings info

Post a Comment

Your e-mail is kept confidential and not included with your comment. Website is optional.