Tutorial: How to create a form in ProcessWire
Form programming in ProcessWire is not complicated, but poorly documented. Guy Verville illustrates the use of the Form API through example:
Programmers are spoiled with ProcessWire because they have access to an excellent form generation tool called FormBuilder. This module, though not free, is affordable, and gives site administrators the freedom to develop their own forms. However, knowing the workings of the Forms API is also essential in a more advanced programming context. Surprisingly, the literature on this subject is rather sparse; this article will attempt to explain it.
The references
- “W3Schools: How TO — Login Form.” It’s never too late to revisit the classics!
- “ProcessWire API Reference.” Especially FormBuilder Class.
- The ProcessWire code: the part we are interested in is wire/modules/Inputfield. Each field is a class that can be invoked.
Note concerning the code in this article
We have activated the call by function of the general variables of PW. The best place to insert the following command is in the config.php file. On this subject, see: “Various ways of accessing the ProcessWire API” : $config->useFunctionsAPI = true;
.
We use Twig as a rendering engine (our template becomes a controller in its own right and the HTML rendering (the view) is moved to the Twig file, hence the call $this->view
. This variable has been defined as global in the main controller MainController.php.
We will therefore have several files, the controller, the view and the css. We will also use files from the core of ProcessWire.
For the purposes of this exercise, we will construct a two-column form.
The steps of a form
Submitting a form is a three-step process:
- Form presentation.
- Validation. Once submitted, the form is first validated to see if the required data are present. If there are any errors, the form is resubmitted to the user with the data already completed.
- Data processing.
Reading now continues through the code comments:
public $values = [];
[...]
public function render(): void {
// The function render() creates immediatly a form.
// It will be empty or filled with given values.
// The function createForm() is explained later.
$form = $this->createForm();
// We check if we have a submitted form.
// The Send button is an excellent choice to check,
// but it could have been another field or a Cancel button.
if(input()->post("pf_submit_btn")) {
// we must first validate
// The processInput function can be intercepted for later validation.
// if an error is found, $form->getErrors() will tell us.
$form->processInput(input()->post);
// Do we have any errors?
if(count($form->getErrors()) > 0) {
// We found errors, we display
// the form again to correct the situation.
// The form is filled with error messages above the appropriate fields.
$this->view->set("form", $form->render());
} else {
// No preliminary errors found. We can treat.
$this->values["company"] = sanitizer()->input()->post("company");
[...]
// Are we satisfied with the values?
// If this is not the case, send the form back to the user!
// This is the rest of the processing code.
[...]
}
} else {
// We don’t have any data yet, we have to ask for it.
// The form is empty by default (except for the date in this example).
// Here we use the Twig rendering engine.
$this->view->set("form", $form->render());
}
}
These steps can be improved by injecting JavaScript to make the user experience more pleasant. We will stick to the raw code as much as possible. The main steps to produce the form fields can be summarized as follows:
- To call the module of the field/form concerned. example:
$form = modules()->get('InputfieldForm");
. - To assign attributes and methods to the variable thus created.
- When it is a field, to add it to the form
($form->add())
or to a container field.
Let's continue reading the code by examining createForm().
protected function createForm() {
// As the form is empty, all values are empty.
// But this form could be filled with previous values if
// the user has made mistakes. $this->values is therefore examined.
if(empty($this->values)) {
$this->values = [
"company" => "",
"city" => "",
"email" => "",
"prlanguages" => "",
"language" => "",
"date" => "",
];
}
// Initialization of the form.
// If nothing is included in "action", the form reloads the same page.
$form = modules()->get("InputfieldForm"); // the form module
$form->action = ""; // we submit on the same page
$form->method = "post"; // the method of submitting values
$form->attr("name+id", "pro_form"); // it will be as much the name as the id
$form->attr("class", "uk-form-stacked no-form-ul"); //any CSS class.
// CSRF
// Hidden field that protects us from CSRF attacks.
$f = modules()->get("InputfieldHidden");
$f->attr("id+name", session()->CSRF->getTokenName());
$f->attr("maxlength", 30);
$f->attr("class", "uk-input");
$f->attr("value", session()->CSRF->getTokenValue());
// Field to integrate any HTML marking.
// Warning, ProcessWire will correct any unclosed tags.
$markup = modules()->get('InputfieldMarkup');
$markup->value = "<div class='warning'>" . __("All fields are required") . "</div>";
$form->add($markup);
Once the form has been created, it can be assigned fields.
// Our form will be in two columns,
// Let’s create both fieldsets with the InputfieldFieldset module.
$firstCol = modules()->get("InputfieldFieldset");
$firstCol->attr("class", "firstCol");
$form->add($firstCol);
$secondCol = modules()->get("InputfieldFieldset");
$secondCol->attr("class", "secondCol");
$form->add($secondCol);
// Text field.
// Do not forget to put the various field labels in the translation tag.
// The first fields go in the left column.
$f = modules()->get("InputfieldText");
$f->set("label", __("Designer’s / architect’s office"));
$f->attr("name+id", "company");
$f->attr("maxlength", 50);
$f->attr("class", "uk-form-width-medium uk-input");
$f->attr("value", $this->values["company"]);
$f->required(true);
$f->attr("placeholder", __('Name of the company'));
$firstCol->add($f); // coudl be $form->add($f)
// Select type field.
// The options can come from ProcessWire.
$options = [
'montreal' => __("Montreal"),
'quebec' => __("Quebec City")
];
$f = modules()->get("InputfieldSelect");
$f->set("label", __("City"));
$f->attr("name+id", "city");
$f->attr("maxlength", 50);
$f->attr("class", "uk-form-width-medium uk-select");
$f->attr("value", $this->values["city"]);
$f->addOptions($options);
$f->required(true);
$firstCol->add($f);
// E-mail address field.
// Processwire will automatically put a repetition field of the address.
$f = wire("modules")->get("InputfieldEmail");
$f->set("label", __("Email"));
$f->set('confirm', 1);
$f->attr("name+id", "email");
$f->attr("maxlength", 60);
$f->attr("class", "uk-form-width-medium");
$f->attr("value", $this->values["email"]);
$f->required = true;
$firstCol->add($f);
// Select field with JavaScript autocomplete.
// Values can come from ProcessWire.
// However, these options will be sent to the Twig engine for JavaScript processing.
// We add from here in the second column.
$optionsLanguages = '
"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"ColdFusion",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"';
$f = modules()->get("InputfieldText");
$f->set("label", __("Programming languages"));
$f->attr("name+id", "prlanguages");
$f->attr("maxlength", 50);
$f->attr("class", "uk-form-width-medium");
$f->attr("value", $this->values["prlanguages"]);
$f->wrapClass("autocomplete");
$f->required(true);
$secondCol->add($f);
// "Radio" type field.
// Options can come from ProcessWire.
// Notice the option "optionColumns".
// If 0, ul class = InputfieldRadiosStacked. If 1, ul class = InputfieldRadiosFloated
$options = [
'fr' => __("French"),
'en' => __("English")
];
$f = modules()->get("InputfieldRadios");
$f->set("label", __("Language"));
$f->attr("name+id", "language");
$f->attr("maxlength", 50);
$f->attr("class", "uk-form-width-medium uk-radio");
$f->attr("value", $this->values["language"]);
$f->set('optionColumns', 1);
$f->addOptions($options);
$f->required(true);
$secondCol->add($f);
// Date field with JavaScript selector.
$f = modules()->get("InputfieldDatetime");
$f->set("label", __("Date"));
$f->attr("name+id", "date");
$f->attr("maxlength", 50);
$f->attr("class", "uk-form-width-medium ");
$f->attr("value", $this->values["date"]);
$f->datepicker = 3; //this is a popup calendar
$f->dateInputFormat = "Y-m-d";
$f->attr("value", time());
$f->required(true);
$f->prependMarkup("<div id='datepicker'>"); // The field is surrounded by a class.
$f->appendMarkup("</div>"); // we must close the markup.
$secondCol->add($f);
// Textarea.
// This last field is directly attached to $form so
// that it will take all the space required by CSS.
$f = modules()->get("InputfieldTextarea");
$f->set("label", __("Your message"));
$f->attr("name+id", "message");
$f->attr("rows", 10);
$f->attr("class", "uk-form-width-medium uk-input");
$f->attr("value", $this->values["message"]);
$f->required(true);
$f->attr("placeholder", __('Your message'));
$form->add($f);
// Submit.
// A form is nothing without a send button!
$f = modules()->get("InputfieldSubmit");
$f->attr("id", "pf-submit-btn");
$f->attr("name", "pf_submit_btn");
$f->attr("value", __("Continue"));
$f->attr("class", "blue-btn large");
$form->add($f);
// Our programming language options are sent
// to the Twig engine.
$this->view->set("optionsLanguages", $optionsLanguages);
return $form;
}
The presentation of the form in Twig
The various JavaScript calls necessary to activate certain properties are inserted in the presentation file (view). The form itself is transmitted by the variable {{form}}
. This can be integrated in a more elegant way, as demonstrated below. Notice the calls of the different ProcessWire Jquery scripts. The last script comes from W3shools. Everything is possible here, including more consistent management with current UX/UI standards.
<link rel="stylesheet"
href="/wire/modules/Inputfield/InputfieldDatetime/timepicker/jquery-ui-timepicker-addon.min.css"/>
<div class="basic-page">{{ page.body | raw }}</div>
<div class="ui-widget">
{{ form | raw }}
</div>
<script src="/wire/modules/Jquery/JqueryCore/JqueryCore.js"></script>
<script src="/wire/modules/Jquery/JqueryUI/JqueryUI.js"></script>
<script type='text/javascript'
src='/wire/modules/Inputfield/InputfieldDatetime/InputfieldDatetime.min.js?v=106-1529786878'></script>
<script src="/wire/modules/Inputfield/InputfieldDatetime/timepicker/jquery-ui-timepicker-addon.min.js"></script>
{% if language == 'francais' %}
<script src="/wire/modules/Inputfield/InputfieldDatetime/timepicker/i18n/jquery-ui-timepicker-fr.js"></script>
<script src="/wire/modules/Jquery/JqueryUI/i18n/jquery.ui.datepicker-fr.js"></script>
{% endif %}
{#Autocomplete script from https://www.w3schools.com/howto/howto_js_autocomplete.asp#}
{#Can be placed, of course, into a file#}
<script>
$(function () {
var availableTags = [{{ optionsLanguages | raw }}];
autocomplete(document.getElementById("prlanguages"), availableTags);
});
function autocomplete(inp, arr) {
/*the autocomplete function takes two arguments,
the text field element and an array of possible autocompleted values:*/
var currentFocus;
/*execute a function when someone writes in the text field:*/
inp.addEventListener("input", function (e) {
var a, b, i, val = this.value;
/*close any open lists of autocompleted values*/
closeAllLists();
if (!val) {
return false;
}
currentFocus = -1;
/*create a DIV element that will contain the items (values):*/
a = document.createElement("DIV");
a.setAttribute("id", this.id + "autocomplete-list");
a.setAttribute("class", "autocomplete-items");
/*append the DIV element as a child of the autocomplete container:*/
this.parentNode.appendChild(a);
/*for each item in the array...*/
for (i = 0; i < arr.length; i++) {
/*check if the item starts with the same letters as the text field value:*/
if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
/*create a DIV element for each matching element:*/
b = document.createElement("DIV");
/*make the matching letters bold:*/
b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
b.innerHTML += arr[i].substr(val.length);
/*insert a input field that will hold the current array item’s value:*/
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
/*execute a function when someone clicks on the item value (DIV element):*/
b.addEventListener("click", function (e) {
/*insert the value for the autocomplete text field:*/
inp.value = this.getElementsByTagName("input")[0].value;
/*close the list of autocompleted values,
(or any other open lists of autocompleted values:*/
closeAllLists();
});
a.appendChild(b);
}
}
});
/*execute a function presses a key on the keyboard:*/
inp.addEventListener("keydown", function (e) {
var x = document.getElementById(this.id + "autocomplete-list");
if (x) x = x.getElementsByTagName("div");
if (e.keyCode == 40) {
/*If the DOWN arrow key is pressed,
increase the currentFocus variable:*/
currentFocus++;
/*and make the current item more visible:*/
addActive(x);
} else if (e.keyCode == 38) { //up
/*If the UP arrow key is pressed,
decrease the currentFocus variable:*/
currentFocus--;
/*and make the current item more visible:*/
addActive(x);
} else if (e.keyCode == 13) {
/*If the ENTER key is pressed, prevent the form from being submitted,*/
e.preventDefault();
if (currentFocus > -1) {
/*and simulate a click on the "active" item:*/
if (x) x[currentFocus].click();
}
}
});
function addActive(x) {
/*a function to classify an item as "active":*/
if (!x) return false;
/*start by removing the "active" class on all items:*/
removeActive(x);
if (currentFocus >= x.length) currentFocus = 0;
if (currentFocus < 0) currentFocus = (x.length - 1);
/*add class "autocomplete-active":*/
x[currentFocus].classList.add("autocomplete-active");
}
function removeActive(x) {
/*a function to remove the "active" class from all autocomplete items:*/
for (var i = 0; i < x.length; i++) {
x[i].classList.remove("autocomplete-active");
}
}
function closeAllLists(elmnt) {
/*close all autocomplete lists in the document,
except the one passed as an argument:*/
var x = document.getElementsByClassName("autocomplete-items");
for (var i = 0; i < x.length; i++) {
if (elmnt != x[i] && elmnt != inp) {
x[i].parentNode.removeChild(x[i]);
}
}
}
/*execute a function when someone clicks in the document:*/
document.addEventListener("click", function (e) {
closeAllLists(e.target);
});
}
</script>
Transmission of files
Philipp Urlich, a well-known programmer in the ProcessWire world, has already published an example of file upload. The InputFileField works in the same way as the others. The processing of the file differs and must be carefully designed. Depending on the type of form requiring such transmission, programming will differ. The Philipp’s example creates a ProcessWire page. The W3schools tutorial provides another example of treatment.
Remember here that the form must be treated as a security breach in a website. This applies especially to files!