{"id":351454,"date":"2021-09-14T07:36:24","date_gmt":"2021-09-14T14:36:24","guid":{"rendered":"https:\/\/css-tricks.com\/?p=351454"},"modified":"2021-09-14T07:36:27","modified_gmt":"2021-09-14T14:36:27","slug":"building-a-form-in-php-using-domdocument","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/building-a-form-in-php-using-domdocument\/","title":{"rendered":"Building a Form in PHP Using DOMDocument"},"content":{"rendered":"\n
Templating makes the web go round. The synthesis of data and structure into content<\/em>. It\u2019s our coolest superpower as developers \u2014 grab some data, then make it work for us, in whatever presentation we need. An array of objects can become a table, a list of cards, a chart, or whatever we think is most useful to the user. Whether the data is our own blog posts in Markdown files, or on-the-minute global exchange rates, the markup and resulting UX are up to us as front-end developers<\/a>.<\/p>\n\n\n\n PHP is an amazing language for templating, providing many ways to merge data with markup. Let\u2019s get into an example of using data to build out an HTML form in this post.<\/p>\n\n\n\n\n\n\n\n Want to get your hands dirty right away? Jump to the implementation.<\/a><\/p>\n\n\n\n In PHP, we can inline variables into string literals that use double quotes, so if we have a variable For the old-schoolers, there\u2019s All of these options are great, but things can get messy when a lot of inline logic is required. If we need to build compound HTML strings, say a form or navigation, the complexity is potentially infinite<\/a>, since HTML elements can nest inside each other.<\/p>\n\n\n Before we go ahead and do the thing we want to do, it\u2019s worth taking a minute to consider what we don\u2019t<\/em> want to do. Consider the following abridged passage from the scripture of WordPress Core, In order to build out a navigation It\u2019s very easy to end up with string spaghetti when concatenating like this, which is as fun to say as it is painful to maintain.<\/p>\n\n\n\n The essence of the problem is that when we try to reason about HTML elements, we\u2019re not thinking about strings<\/em>. It just so happens that strings are what the browser consumes and PHP outputs. But our mental model is more like the DOM \u2014 elements are arranged into a tree, and each node has many potential attributes, properties, and children.<\/p>\n\n\n\n Wouldn\u2019t it be great if there were a structured, expressive way to build our tree?<\/p>\n\n\n\n Enter…<\/p>\n\n\n PHP 5 added the We start out by initializing a new Now we can add a The string Once we have an element, we can set its attributes:<\/p>\n\n\n\n We can add children to it:<\/p>\n\n\n\n And finally, get the complete HTML string in one go:<\/p>\n\n\n\n Notice how this style of coding keeps our code organized according to our mental model \u2014 a document has elements; elements can have any number of attributes; and elements nest inside one another without needing to know anything about each other. The whole \u201cHTML is just a string\u201d part comes in at the end, once our structure is in place.<\/p>\n\n\n\n The \u201cdocument\u201d here is a bit different from the actual DOM, in that it doesn\u2019t need to represent an entire document, just a block of HTML. In fact, if you need to create two similar elements, you could save a HTML string using Say we need to build a form on the server using data from a CRM provider and our own markup. The API response from the CRM looks like this:<\/p>\n\n\n\n This example doesn\u2019t use the exact data structure of any specific CRM, but it\u2019s rather representative.<\/p>\n\n\n\n And let\u2019s suppose we want our markup to look like this:<\/p>\n\n\n\n What\u2019s that Now that we know what our desired result is, here\u2019s the game plan:<\/p>\n\n\n\n So let\u2019s stub out our process and get some technicalities out of the way:<\/p>\n\n\n\n So far, we\u2019ve gotten the data and parsed it, initialized our Since we\u2019re in a loop, and PHP doesn\u2019t scope variables in loops, we reset the Notice that we set classes using the Since we know that the API will only return specific field types, we can switch over the type and write specific code for each one:<\/p>\n\n\n\n Now let\u2019s handle text areas, single checkboxes and hidden fields:<\/p>\n\n\n\n Notice something new we\u2019re doing for the checkbox and hidden cases? We\u2019re not just creating the Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of And here is the real beauty of using a builder like Let\u2019s add the logic for OK, so there\u2019s a lot going on here, but the underlying logic is the same. After setting up the outer We\u2019re also doing some Now let\u2019s handle So, first we determine if the field set should be for checkboxes or radio button. Then we set the container class accordingly, and build the Notice we use regular PHP string interpolation to set the container class on line 21 and to create a unique ID for each choice on line 30.<\/p>\n\n\n One last type we have to add is slightly more complex than it looks. Many forms include instruction fields, which aren\u2019t inputs but just some HTML we need to print between other fields.<\/p>\n\n\n\n We\u2019ll need to reach for another At this point you might be wondering how we found ourselves with an object called The API we are consuming kindly provides an individual validation message for each required field. If there\u2019s a submission error, we can show the errors inline together with the fields, rather than a generic \u201coops, your bad\u201d message at the bottom.<\/p>\n\n\n\n Let\u2019s add the validation text to each element:<\/p>\n\n\n\n That\u2019s all it takes! No need to fiddle with the field type logic\u2009\u2014\u2009just conditionally build an element for each field.<\/p>\n\n\n So what happens after we build all the field elements? We need to add the Why are we checking if Hey presto, a custom HTML form!<\/p>\n\n\n As you may know, many form builders allow authors to set rows and columns for fields. For example, a row might contain both the first name and last name<\/a> fields, each in a single 50% width column. So how would we go about implementing this, you ask? By exemplifying (once again) how loop-friendly Our API response includes the grid data like this:<\/p>\n\n\n\n We\u2019re assuming that adding a Before we dive in, let\u2019s think through what we need in order to add rows. The basic logic goes something like this:<\/p>\n\n\n\n Now, how would we do this if we were concatenating strings? Probably by adding a string like So what\u2019s the structured way to handle this? Thanks for asking. First let\u2019s add row tracking before our loop and build an additional row container element. Then we\u2019ll make sure to append each container So far we\u2019ve just added another All we need to do is overwrite the Forms are a great use case for object-oriented templating. And thinking about that snippet from WordPress Core, an argument might be made that nested menus are a good use case as well. Any task where the markup follows complex logic makes for a good candidate for this approach. Here\u2019s the entire code snippet for our form.<\/a> Feel free it adapt it to any form API you find yourself dealing with. Here\u2019s the official documentation<\/a>, which is good for getting a sense of the available API.<\/p>\n\n\n\n We didn\u2019t even mention Fun(?) fact:<\/strong> Microsoft Office files that end with The most important thing is to remember that templating is a superpower. Being able to build the right markup for the right situation can be the key to a great UX.<\/p>\n","protected":false},"excerpt":{"rendered":" Learn how to build an HTML form in PHP using DOMDocument \u2014 a structured and expressive way to build logical markup.<\/p>\n","protected":false},"author":274686,"featured_media":351656,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"c2c_always_allow_admin_comments":false,"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[4],"tags":[18979,595,772],"jetpack_publicize_connections":[],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/09\/cover@2x.jpg?fit=2400%2C1200&ssl=1","jetpack-related-posts":[],"featured_media_src_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/09\/cover@2x.jpg?fit=1024%2C512&ssl=1","_links":{"self":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/351454"}],"collection":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/users\/274686"}],"replies":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/comments?post=351454"}],"version-history":[{"count":10,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/351454\/revisions"}],"predecessor-version":[{"id":351782,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/351454\/revisions\/351782"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media\/351656"}],"wp:attachment":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media?parent=351454"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/categories?post=351454"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/tags?post=351454"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}$name = 'world'<\/code>, we can write
echo \"Hello, {$name}\"<\/code>, and it prints the expected
Hello, world<\/code>. For more complex templating, we can always concatenate strings, like:
echo \"Hello, \" . $name . \".\"<\/code>.<\/p>\n\n\n\n
printf(\"Hello, %s\", $name)<\/code>. For multiline strings, you can use Heredoc<\/a> (the one that starts like
<<<MYTEXT<\/code>). And, last but certainly not least, we can sprinkle PHP variables inside HTML, like
<p>Hello, <?= $name ?><\/p><\/code>.<\/p>\n\n\n\n
What we\u2019re trying to avoid<\/h3>\n\n\n
class-walker-nav-menu.php<\/code>, verses 170-270:<\/p>\n\n\n\n
<?php \/\/ class-walker-nav-menu.php\n\/\/ ...\n$output .= $indent . '<li' . $id . $class_names . '>';\n\/\/ ...\n$item_output = $args->before;\n$item_output .= '<a' . $attributes . '>';\n$item_output .= $args->link_before . $title . $args->link_after;\n$item_output .= '<\/a>';\n$item_output .= $args->after;\n\/\/ ...\n$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );\n\/\/ ...\n$output .= \"<\/li>{$n}\";<\/code><\/pre>\n\n\n\n
<ul><\/code> in this function, we use a variable,
$output<\/code>, which is a very long string to which we keep adding stuff. This type of code has a very specific and limited order of operations. If we wanted to add an attribute to the
<a><\/code>, we must have access to
$attributes<\/code> before this runs. And if we wanted to optionally nest a
<span><\/code> or an
<img><\/code> inside the
<a><\/code>, we\u2019d need to author a whole new block of code that would replace the middle of line 7 with about 4-10 new lines, depending on what exactly we want to add. Now imagine you need to optionally add the
<span><\/code> , and then optionally add the
<img><\/code>, either inside the
<span><\/code> or after it. That alone is three
if<\/code> statements, making the code even less legible.<\/p>\n\n\n\n
The
DOMDocument<\/code> class<\/h3>\n\n\n
DOM<\/code> module<\/a> to it\u2019s roster of Not So Strictly Typed™ types. Its main entry point is the
DOMDocument<\/code> class<\/a>, which is intentionally similar to the Web API\u2019s JavaScript
DOM<\/code>. If you\u2019ve ever used
document.createElement<\/code><\/a> or, for those of us of a certain age, jQuery\u2019s
$('<p>Hi there!<\/p>')<\/code> syntax, this will probably feel quite familiar.<\/p>\n\n\n\n
DOMDocument<\/code>:<\/p>\n\n\n\n
$dom = new DOMDocument();<\/code><\/pre>\n\n\n\n
DOMElement<\/code> to it:<\/p>\n\n\n\n
$p = $dom->createElement('p');<\/code><\/pre>\n\n\n\n
'p'<\/code> represents the type of element we want, so other valid strings would be
'div'<\/code>,
'img'<\/code> , etc.<\/p>\n\n\n\n
$p->setAttribute('class', 'headline');<\/code><\/pre>\n\n\n\n
$span = $dom->createElement('span', 'This is a headline'); \/\/ The 2nd argument populates the element's textContent\n$p->appendChild($span);<\/code><\/pre>\n\n\n\n
$dom->appendChild($p);\n$htmlString = $dom->saveHTML();\necho $htmlString;<\/code><\/pre>\n\n\n\n
saveHTML()<\/code>, modify the DOM \u201cdocument\u201d some more, and then save a new HTML string by calling
saveHTML()<\/code> again.<\/p>\n\n\n
Getting data and setting the structure<\/h3>\n\n\n
{\n \"submit_button_label\": \"Submit now!\",\n \"fields\": [\n {\n \"id\": \"first-name\",\n \"type\": \"text\",\n \"label\": \"First name\",\n \"required\": true,\n \"validation_message\": \"First name is required.\",\n \"max_length\": 30\n },\n {\n \"id\": \"category\",\n \"type\": \"multiple_choice\",\n \"label\": \"Choose all categories that apply\",\n \"required\": false,\n \"field_metadata\": {\n \"multi_select\": true,\n \"values\": [\n { \"value\": \"travel\", \"label\": \"Travel\" },\n { \"value\": \"marketing\", \"label\": \"Marketing\" }\n ]\n }\n }\n ]\n}<\/code><\/pre>\n\n\n\n
<form>\n <label class=\"field\">\n <input type=\"text\" name=\"first-name\" id=\"first-name\" placeholder=\" \" required>\n <span class=\"label\">First name<\/span>\n <em class=\"validation\" hidden>First name is required.<\/em>\n <\/label>\n <label class=\"field checkbox-group\">\n <fieldset>\n <div class=\"choice\">\n <input type=\"checkbox\" value=\"travel\" id=\"category-travel\" name=\"category\">\n <label for=\"category-travel\">Travel<\/label>\n <\/div>\n <div class=\"choice\">\n <input type=\"checkbox\" value=\"marketing\" id=\"category-marketing\" name=\"category\">\n <label for=\"category-marketing\">Marketing<\/label>\n <\/div>\n <\/fieldset>\n <span class=\"label\">Choose all categories that apply<\/span>\n <\/label>\n<\/form><\/code><\/pre>\n\n\n\n
placeholder=\" \"<\/code>? It\u2019s a small trick that allows us to track in CSS whether the field is empty, without needing JavaScript. As long as the input is empty, it matches
input:placeholder-shown<\/code>, but the user doesn’t see any visible placeholder text. Just the kind of thing you can do when we control the markup!<\/p>\n\n\n\n
DOMDocument<\/code><\/li>
<?php\nfunction renderForm ($endpoint) {\n \/\/ Get the data from the API and convert it to a PHP object\n $formResult = file_get_contents($endpoint);\n $formContent = json_decode($formResult);\n $formFields = $formContent->fields;\n\n \/\/ Start building the DOM\n $dom = new DOMDocument();\n $form = $dom->createElement('form');\n\n \/\/ Iterate over the fields and build each one\n foreach ($formFields as $field) {\n \/\/ TODO: Do something with the field data\n }\n\n \/\/ Get the HTML output\n $dom->appendChild($form);\n $htmlString = $dom->saveHTML();\n echo $htmlString;\n}<\/code><\/pre>\n\n\n\n
DOMDocument<\/code> and echoed its output. What do we want to do for each field? First off, let\u2019s build the container element which, in our example, should be a
<label><\/code>, and the labelling
<span><\/code> which is common to all field types:<\/p>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n if ($field->label) {\n $label = $dom->createElement('span', $field->label);\n $label->setAttribute('class', 'label');\n }\n\n \/\/ Add the label to the `<label>`\n if ($label) $element->appendChild($label);\n}<\/code><\/pre>\n\n\n\n
$label<\/code> element on each iteration. Then, if the field has a label, we build the element. At the end, we append it to the container element.<\/p>\n\n\n\n
setAttribute<\/code> method. Unlike the Web API, there unfortunately is no special handing of class lists. They\u2019re just another attribute. If we had some really complex class logic, since It\u2019s Just PHP™, we could create an array and then implode it:
$label->setAttribute('class', implode($labelClassList))<\/code>.<\/p>\n\n\n
Single inputs<\/h3>\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Build the input element\n switch ($field->type) {\n case 'text':\n case 'email':\n case 'telephone':\n $input = $dom->createElement('input');\n $input->setAttribute('placeholder', ' ');\n if ($field->type === 'email') $input->setAttribute('type', 'email');\n if ($field->type === 'telephone') $input->setAttribute('type', 'tel');\n break;\n }\n}<\/code><\/pre>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/... \n case 'text_area':\n $input = $dom->createElement('textarea');\n $input->setAttribute('placeholder', ' ');\n if ($rows = $field->field_metadata->rows) $input->setAttribute('rows', $rows);\n break;\n\n case 'checkbox':\n $element->setAttribute('class', 'field single-checkbox');\n $input = $dom->createElement('input');\n $input->setAttribute('type', 'checkbox');\n if ($field->field_metadata->initially_checked === true) $input->setAttribute('checked', 'checked');\n break;\n\n case 'hidden':\n $input = $dom->createElement('input');\n $input->setAttribute('type', 'hidden');\n $input->setAttribute('value', $field->field_metadata->value);\n $element->setAttribute('hidden', 'hidden');\n $element->setAttribute('style', 'display: none;');\n $label->textContent = '';\n break;\n }\n}<\/code><\/pre>\n\n\n\n
<input><\/code> element; we\u2019re making changes to the container<\/em>
<label><\/code> element<\/em>! For a single checkbox field we want to modify the class of the container, so we can align the checkbox and label horizontally; a hidden
<input><\/code>‘s container should also be completely hidden.<\/p>\n\n\n\n
if<\/code> statements regarding the type of element and its metadata in the top of the block. Or, maybe worse, we start the
switch<\/code> way earlier, then copy-paste a lot of common code between each branch.<\/p>\n\n\n\n
DOMDocument<\/code> \u2014 until we hit that
saveHTML()<\/code>, everything is still editable, and everything is still structured.<\/p>\n\n\n
Nested looping elements<\/h3>\n\n\n
<select><\/code> elements:<\/p>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/... \n case 'select':\n $element->setAttribute('class', 'field select');\n $input = $dom->createElement('select');\n $input->setAttribute('required', 'required');\n if ($field->field_metadata->multi_select === true)\n $input->setAttribute('multiple', 'multiple');\n \n $options = [];\n \n \/\/ Track whether there's a pre-selected option\n $optionSelected = false;\n \n foreach ($field->field_metadata->values as $value) {\n $option = $dom->createElement('option', htmlspecialchars($value->label));\n \n \/\/ Bail if there's no value\n if (!$value->value) continue;\n \n \/\/ Set pre-selected option\n if ($value->selected === true) {\n $option->setAttribute('selected', 'selected');\n $optionSelected = true;\n }\n $option->setAttribute('value', $value->value);\n $options[] = $option;\n }\n \n \/\/ If there is no pre-selected option, build an empty placeholder option\n if ($optionSelected === false) {\n $emptyOption = $dom->createElement('option');\n \n \/\/ Set option to hidden, disabled, and selected\n foreach (['hidden', 'disabled', 'selected'] as $attribute)\n $emptyOption->setAttribute($attribute, $attribute);\n $input->appendChild($emptyOption);\n }\n \n \/\/ Add options from array to `<select>`\n foreach ($options as $option) {\n $input->appendChild($option);\n }\n break;\n }\n}<\/code><\/pre>\n\n\n\n
<select><\/code>, we make an array of
<option><\/code>s to append inside it.<\/p>\n\n\n\n
<select><\/code>-specific trickery here: If there is no pre-selected option, we add an empty placeholder option that is already selected, but can\u2019t be selected by the user. The goal is to place our
<label class=\"label\"><\/code> as a \u201cplaceholder\u201d using CSS, but this technique can be useful for all kinds of designs. By appending it to the
$input<\/code> before appending the other options, we make sure it is the first option in the markup.<\/p>\n\n\n\n
<fieldset><\/code>s of radio buttons and checkboxes:<\/p>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/ ... \n case 'multiple_choice':\n $choiceType = $field->field_metadata->multi_select === true ? 'checkbox' : 'radio';\n $element->setAttribute('class', \"field {$choiceType}-group\");\n $input = $dom->createElement('fieldset');\n \n \/\/ Build a choice `<input>` for each option in the fieldset\n foreach ($field->field_metadata->values as $choiceValue) {\n $choiceField = $dom->createElement('div');\n $choiceField->setAttribute('class', 'choice');\n \n \/\/ Set a unique ID using the field ID + the choice ID\n $choiceID = \"{$field->id}-{$choiceValue->value}\";\n \n \/\/ Build the `<input>` element\n $choice = $dom->createElement('input');\n $choice->setAttribute('type', $choiceType);\n $choice->setAttribute('value', $choiceValue->value);\n $choice->setAttribute('id', $choiceID);\n $choice->setAttribute('name', $field->id);\n $choiceField->appendChild($choice);\n \n \/\/ Build the `<label>` element\n $choiceLabel = $dom->createElement('label', $choiceValue->label);\n $choiceLabel->setAttribute('for', $choiceID);\n $choiceField->appendChild($choiceLabel);\n \n $input->appendChild($choiceField);\n }\n break;\n }\n}<\/code><\/pre>\n\n\n\n
<fieldset><\/code>. After that, we iterate over the available choices and build a
<div><\/code> for each one with an
<input><\/code> and a
<label><\/code>.<\/p>\n\n\n\n
Fragments<\/h3>\n\n\n
DOMDocument<\/code> method,
createDocumentFragment()<\/code>. This allows us to add arbitrary HTML without using the DOM structuring:<\/p>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/... \n case 'instruction':\n $element->setAttribute('class', 'field text');\n $fragment = $dom->createDocumentFragment();\n $fragment->appendXML($field->text);\n $input = $dom->createElement('p');\n $input->appendChild($fragment);\n break;\n }\n}<\/code><\/pre>\n\n\n\n
$input<\/code>, which actually represents a static
<p><\/code> element. The goal is to use a common variable name for each iteration of the fields loop, so at the end we can always add it using
$element->appendChild($input)<\/code> regardless of the actual field type. So, yeah, naming things is hard<\/a>.<\/p>\n\n\n
Validation<\/h3>\n\n\n
<?php\n\/\/ ...\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n\n \/\/ Reset input values\n $input = null;\n $label = null;\n $validation = null;\n\n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n\n \/\/ Add a `<em>` for the validation message if it is set\n if (isset($field->validation_message)) {\n $validation = $dom->createElement('em');\n $fragment = $dom->createDocumentFragment();\n $fragment->appendXML($field->validation_message);\n $validation->appendChild($fragment);\n $validation->setAttribute('class', 'validation-message');\n $validation->setAttribute('hidden', 'hidden'); \/\/ Initially hidden, and will be unhidden with Javascript if there's an error on the field\n }\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/ ...\n }\n}<\/code><\/pre>\n\n\n\n
Bringing it all together<\/h3>\n\n\n
$input<\/code>,
$label<\/code>, and
$validation<\/code> objects to the DOM tree we\u2019re building. We can also use the opportunity to add common attributes, like
required<\/code>. Then we\u2019ll add the submit button, which is separate from the fields in this API.<\/p>\n\n\n\n
<?php\nfunction renderForm ($endpoint) {\n \/\/ Get the data from the API and convert it to a PHP object\n \/\/ ...\n\n \/\/ Start building the DOM\n $dom = new DOMDocument();\n $form = $dom->createElement('form');\n\n \/\/ Iterate over the fields and build each one\n foreach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n \n \/\/ Reset input values\n $input = null;\n $label = null;\n $validation = null;\n \n \/\/ Add a `<span>` for the label if it is set\n \/\/ ...\n \n \/\/ Add a `<em>` for the validation message if it is set\n \/\/ ...\n \n \/\/ Build the input element\n switch ($field->type) {\n \/\/ ...\n }\n \n \/\/ Add the input element\n if ($input) {\n $input->setAttribute('id', $field->id);\n if ($field->required)\n $input->setAttribute('required', 'required');\n if (isset($field->max_length))\n $input->setAttribute('maxlength', $field->max_length);\n $element->appendChild($input);\n \n if ($label)\n $element->appendChild($label);\n \n if ($validation)\n $element->appendChild($validation);\n \n $form->appendChild($element);\n }\n }\n \n \/\/ Build the submit button\n $submitButtonLabel = $formContent->submit_button_label;\n $submitButtonField = $dom->createElement('div');\n $submitButtonField->setAttribute('class', 'field submit');\n $submitButton = $dom->createElement('button', $submitButtonLabel);\n $submitButtonField->appendChild($submitButton);\n $form->appendChild($submitButtonField);\n\n \/\/ Get the HTML output\n $dom->appendChild($form);\n $htmlString = $dom->saveHTML();\n echo $htmlString;\n}<\/code><\/pre>\n\n\n\n
$input<\/code> is truthy? Since we reset it to
null<\/code> at the top of the loop, and only build it if the type conforms to our expected switch cases, this ensures we don\u2019t accidentally include unexpected elements our code can\u2019t handle properly.<\/p>\n\n\n\n
Bonus points: rows and columns<\/h3>\n\n\n
DOMDocument<\/code> is, of course!<\/p>\n\n\n\n
{\n \"submit_button_label\": \"Submit now!\",\n \"fields\": [\n {\n \"id\": \"first-name\",\n \"type\": \"text\",\n \"label\": \"First name\",\n \"required\": true,\n \"validation_message\": \"First name is required.\",\n \"max_length\": 30,\n \"row\": 1,\n \"column\": 1\n },\n {\n \"id\": \"category\",\n \"type\": \"multiple_choice\",\n \"label\": \"Choose all categories that apply\",\n \"required\": false,\n \"field_metadata\": {\n \"multi_select\": true,\n \"values\": [\n { \"value\": \"travel\", \"label\": \"Travel\" },\n { \"value\": \"marketing\", \"label\": \"Marketing\" }\n ]\n },\n \"row\": 2,\n \"column\": 1\n }\n ]\n}<\/code><\/pre>\n\n\n\n
data-column<\/code> attribute is enough for styling the width, but that each row needs to be it\u2019s own element (i.e. no CSS grid).<\/p>\n\n\n\n
'<\/div><div class=\"row\">'<\/code> whenever we reach a new row. This kind of \u201creversed HTML string\u201d is always very confusing to me, so I can only imagine how my IDE feels. And the cherry on top is that thanks to the browser auto-closing open tags, a single typo will result in a gazillion nested
<div><\/code>s. Just like fun, but the opposite.<\/p>\n\n\n\n
$element<\/code> to its
$rowElement<\/code> rather than directly to
$form<\/code>.<\/p>\n\n\n\n
<?php\nfunction renderForm ($endpoint) {\n \/\/ Get the data from the API and convert it to a PHP object\n \/\/ ...\n\n \/\/ Start building the DOM\n $dom = new DOMDocument();\n $form = $dom->createElement('form');\n\n \/\/ init tracking of rows\n $row = 0;\n $rowElement = $dom->createElement('div');\n $rowElement->setAttribute('class', 'field-row');\n\n \/\/ Iterate over the fields and build each one\n foreach ($formFields as $field) {\n \/\/ Build the container `<label>`\n $element = $dom->createElement('label');\n $element->setAttribute('class', 'field');\n $element->setAttribute('data-row', $field->row);\n $element->setAttribute('data-column', $field->column);\n \n \/\/ Add the input element to the row\n if ($input) {\n \/\/ ...\n $rowElement->appendChild($element);\n $form->appendChild($rowElement);\n }\n }\n \/\/ ...\n}<\/code><\/pre>\n\n\n\n
<div><\/code> around the fields. Let\u2019s build a new<\/em> row element for each row inside the loop:<\/p>\n\n\n\n
<?php\n\/\/ ...\n\/\/ Init tracking of rows\n$row = 0;\n$rowElement = $dom->createElement('div');\n$rowElement->setAttribute('class', 'field-row');\n\n\/\/ Iterate over the fields and build each one\nforeach ($formFields as $field) {\n \/\/ ...\n \/\/ If we've reached a new row, create a new $rowElement\n if ($field->row > $row) {\n $row = $field->row;\n $rowElement = $dom->createElement('div');\n $rowElement->setAttribute('class', 'field-row');\n }\n\n \/\/ Build the input element\n switch ($field->type) {\n \/\/ ... \n \/\/ Add the input element to the row\n if ($input) {\n \/\/ ...\n $rowElement->appendChild($element);\n\n \/\/ Automatically de-duped\n $form->appendChild($rowElement);\n }\n }\n}<\/code><\/pre>\n\n\n\n
$rowElement<\/code> object as a new DOM element, and PHP treats it as a new unique object. So, at the end of every loop, we just append whatever the current
$rowElement<\/code> is \u2014 if it\u2019s still the same one as in the previous iteration, then the form is updated; if it\u2019s a new element, it is appended at the end.<\/p>\n\n\n
Where do we go from here?<\/h3>\n\n\n
DOMDocument<\/code> can output any XML, so you could also use it to build an RSS feed from posts data.<\/p>\n\n\n\n
DOMDocument<\/code> can parse existing HTML and XML<\/a>. You can then look up elements using the XPath API<\/a>, which is kinda similar to
document.querySelector<\/code>, or
cheerio<\/code> on Node.js. There\u2019s a bit of a learning curve, but it\u2019s a super powerful API for handling external content.<\/p>\n\n\n\n
x<\/code> (e.g.
.xlsx<\/code>) are XML files. Don\u2019t tell the marketing department, but it\u2019s possible to parse Word docs and output HTML on the server.<\/p>\n\n\n\n