{"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 $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

For the old-schoolers, there\u2019s 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

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

What we\u2019re trying to avoid<\/h3>\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, 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

In order to build out a navigation <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

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

The DOMDocument<\/code> class<\/h3>\n\n\n

PHP 5 added the 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

We start out by initializing a new DOMDocument<\/code>:<\/p>\n\n\n\n

$dom = new DOMDocument();<\/code><\/pre>\n\n\n\n

Now we can add a DOMElement<\/code> to it:<\/p>\n\n\n\n

$p = $dom->createElement('p');<\/code><\/pre>\n\n\n\n

The string '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

Once we have an element, we can set its attributes:<\/p>\n\n\n\n

$p->setAttribute('class', 'headline');<\/code><\/pre>\n\n\n\n

We can add children to it:<\/p>\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

And finally, get the complete HTML string in one go:<\/p>\n\n\n\n

$dom->appendChild($p);\n$htmlString = $dom->saveHTML();\necho $htmlString;<\/code><\/pre>\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 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

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

{\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

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

<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

What\u2019s that 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

Now that we know what our desired result is, here\u2019s the game plan:<\/p>\n\n\n\n

  1. Get the field definitions and other content from the API<\/li>
  2. Initialize a DOMDocument<\/code><\/li>
  3. Iterate over the fields and build each one as required<\/li>
  4. Get the HTML output<\/li><\/ol>\n\n\n\n

    So let\u2019s stub out our process and get some technicalities out of the way:<\/p>\n\n\n\n

    <?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

    So far, we\u2019ve gotten the data and parsed it, initialized our 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

    Since we\u2019re in a loop, and PHP doesn\u2019t scope variables in loops, we reset the $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

    Notice that we set classes using the 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

    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

    <?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

    Now let\u2019s handle text areas, single checkboxes and hidden fields:<\/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 '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

    Notice something new we\u2019re doing for the checkbox and hidden cases? We\u2019re not just creating the <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

    Now if we were merely concatenating strings, it would be impossible to change at this point. We would have to add a bunch of 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

    And here is the real beauty of using a builder like 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

    Let\u2019s add the logic for <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

    OK, so there\u2019s a lot going on here, but the underlying logic is the same. After setting up the outer <select><\/code>, we make an array of <option><\/code>s to append inside it.<\/p>\n\n\n\n

    We\u2019re also doing some <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

    Now let\u2019s handle <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

    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 <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

    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

    Fragments<\/h3>\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 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

    At this point you might be wondering how we found ourselves with an object called $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

    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

    <?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

    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

    Bringing it all together<\/h3>\n\n\n

    So what happens after we build all the field elements? We need to add the $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

    Why are we checking if $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

    Hey presto, a custom HTML form!<\/p>\n\n\n

    Bonus points: rows and columns<\/h3>\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 DOMDocument<\/code> is, of course!<\/p>\n\n\n\n

    Our API response includes the grid data like this:<\/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

    We\u2019re assuming that adding a 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

    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

    1. Track the latest row encountered.<\/li>
    2. If the current row is larger, i.e. we\u2019ve jumped to the next row, create a new row element and start adding to it instead of the previous one.<\/li><\/ol>\n\n\n\n

      Now, how would we do this if we were concatenating strings? Probably by adding a string like '<\/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

      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 $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

      So far we\u2019ve just added another <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

      All we need to do is overwrite the $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

      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. 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

      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 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

      Fun(?) fact:<\/strong> Microsoft Office files that end with 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

      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}]}}