Erstellung eines Formulars in PHP mit DOMDocument

Avatar of Jonathan Land
Jonathan Land am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Templating bringt das Web in Schwung. Die Synthese von Daten und Struktur zu Inhalt. Das ist unsere coolste Superkraft als Entwickler — schnappen Sie sich einige Daten und lassen Sie sie für sich arbeiten, in jeder Präsentation, die wir brauchen. Ein Array von Objekten kann zu einer Tabelle, einer Liste von Karten, einem Diagramm oder was auch immer wir für den Benutzer am nützlichsten halten, werden. Ob die Daten unsere eigenen Blog-Posts in Markdown-Dateien sind oder minutengenaue globale Wechselkurse, das Markup und die resultierende UX liegen bei uns als Front-End-Entwicklern.

PHP ist eine erstaunliche Sprache für Templating und bietet viele Möglichkeiten, Daten mit Markup zu verschmelzen. Lassen Sie uns in diesem Beitrag ein Beispiel für die Verwendung von Daten zur Erstellung eines HTML-Formulars betrachten.

Möchten Sie sofort loslegen? Springen Sie zur Implementierung.

In PHP können wir Variablen in Zeichenfolgenliterale einfügen, die doppelte Anführungszeichen verwenden. Wenn wir also eine Variable $name = 'world' haben, können wir echo "Hello, {$name}" schreiben, und es wird die erwartete Hello, world ausgegeben. Für komplexere Vorlagen können wir immer Zeichenfolgen verketten, z. B.: echo "Hello, " . $name . ".".

Für die Old-Schooler gibt es printf("Hello, %s", $name). Für mehrzeilige Zeichenfolgen können Sie Heredoc verwenden (dasjenige, das wie <<<MYTEXT beginnt). Und nicht zuletzt können wir PHP-Variablen in HTML einstreuen, wie z. B. <p>Hello, <?= $name ?></p>.

All diese Optionen sind großartig, aber die Dinge können unübersichtlich werden, wenn viel Inline-Logik erforderlich ist. Wenn wir zusammengesetzte HTML-Zeichenfolgen erstellen müssen, z. B. ein Formular oder eine Navigation, ist die Komplexität potenziell unendlich, da HTML-Elemente ineinander verschachtelt sein können.

Was wir vermeiden wollen

Bevor wir weitermachen und das tun, was wir tun wollen, lohnt es sich, kurz darüber nachzudenken, was wir nicht tun wollen. Betrachten Sie die folgende gekürzte Passage aus der Schrift des WordPress Core, class-walker-nav-menu.php, Verse 170-270

<?php // class-walker-nav-menu.php
// ...
$output .= $indent . '<li' . $id . $class_names . '>';
// ...
$item_output  = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
// ...
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
// ...
$output .= "</li>{$n}";

Um in dieser Funktion eine Navigations-<ul> zu erstellen, verwenden wir eine Variable, $output, die eine sehr lange Zeichenfolge ist, zu der wir immer wieder Dinge hinzufügen. Diese Art von Code hat eine sehr spezifische und begrenzte Reihenfolge der Operationen. Wenn wir ein Attribut zu <a> hinzufügen wollten, müssen wir Zugriff auf $attributes haben, bevor dies ausgeführt wird. Und wenn wir optional einen <span> oder ein <img> in <a> verschachteln wollten, müssten wir einen komplett neuen Codeblock erstellen, der die Mitte von Zeile 7 durch etwa 4-10 neue Zeilen ersetzt, je nachdem, was genau wir hinzufügen möchten. Stellen Sie sich nun vor, Sie müssen optional den <span> hinzufügen und dann optional das <img>, entweder innerhalb des <span> oder danach. Das allein sind drei if-Anweisungen, die den Code noch weniger lesbar machen.

Es ist sehr einfach, beim Verketten auf diese Weise mit String-Spaghetti zu enden, was genauso Spaß macht zu sagen wie schmerzhaft zu warten ist.

Das Wesen des Problems ist, dass wir, wenn wir versuchen, HTML-Elemente zu verstehen, nicht über Zeichenfolgen nachdenken. Es stellt sich nur heraus, dass Zeichenfolgen das sind, was der Browser verbraucht und PHP ausgibt. Aber unser mentales Modell ähnelt eher dem DOM — Elemente sind in einem Baum angeordnet, und jeder Knoten hat viele potenzielle Attribute, Eigenschaften und Kinder.

Wäre es nicht großartig, wenn es eine strukturierte, ausdrucksstarke Möglichkeit gäbe, unseren Baum zu bauen?

Treten Sie ein...

Die DOMDocument-Klasse

PHP 5 fügte das DOM-Modul seinem Repertoire an nicht ganz streng typisierten™ Typen hinzu. Sein Haupteinstiegspunkt ist die DOMDocument-Klasse, die absichtlich der JavaScript-DOM der Web-API ähnelt. Wenn Sie jemals document.createElement oder, für uns von einem bestimmten Alter, die jQuery-Syntax $('<p>Hi there!</p>') verwendet haben, wird sich das wahrscheinlich recht vertraut anfühlen.

Wir beginnen mit der Initialisierung eines neuen DOMDocument

$dom = new DOMDocument();

Jetzt können wir ein DOMElement hinzufügen

$p = $dom->createElement('p');

Die Zeichenfolge 'p' repräsentiert den Elementtyp, den wir wünschen, daher wären andere gültige Zeichenfolgen 'div', 'img' usw.

Sobald wir ein Element haben, können wir seine Attribute festlegen

$p->setAttribute('class', 'headline');

Wir können ihm Kinder hinzufügen

$span = $dom->createElement('span', 'This is a headline'); // The 2nd argument populates the element's textContent
$p->appendChild($span);

Und schließlich die vollständige HTML-Zeichenfolge auf einmal abrufen

$dom->appendChild($p);
$htmlString = $dom->saveHTML();
echo $htmlString;

Beachten Sie, wie diese Art des Codierens unseren Code gemäß unserem mentalen Modell organisiert hält — ein Dokument hat Elemente; Elemente können eine beliebige Anzahl von Attributen haben; und Elemente verschachteln sich ineinander, ohne etwas voneinander wissen zu müssen. Der ganze Teil "HTML ist nur eine Zeichenfolge" kommt am Ende, sobald unsere Struktur steht.

Das "Dokument" hier ist etwas anders als das tatsächliche DOM, da es nicht ein ganzes Dokument darstellen muss, sondern nur einen HTML-Block. Tatsächlich könnten Sie, wenn Sie zwei ähnliche Elemente erstellen müssen, eine HTML-Zeichenfolge mit saveHTML() speichern, das DOM- "Dokument" weiter modifizieren und dann eine neue HTML-Zeichenfolge speichern, indem Sie erneut saveHTML() aufrufen.

Daten abrufen und Struktur festlegen

Sagen wir, wir müssen ein Formular auf dem Server mit Daten von einem CRM-Anbieter und unserem eigenen Markup erstellen. Die API-Antwort vom CRM sieht so aus

{
  "submit_button_label": "Submit now!",
  "fields": [
    {
      "id": "first-name",
      "type": "text",
      "label": "First name",
      "required": true,
      "validation_message": "First name is required.",
      "max_length": 30
    },
    {
      "id": "category",
      "type": "multiple_choice",
      "label": "Choose all categories that apply",
      "required": false,
      "field_metadata": {
        "multi_select": true,
        "values": [
          { "value": "travel", "label": "Travel" },
          { "value": "marketing", "label": "Marketing" }
        ]
      }
    }
  ]
}

Dieses Beispiel verwendet nicht die exakte Datenstruktur eines bestimmten CRM, sondern ist eher repräsentativ.

Und nehmen wir an, unser Markup soll so aussehen

<form>
  <label class="field">
    <input type="text" name="first-name" id="first-name" placeholder=" " required>
    <span class="label">First name</span>
    <em class="validation" hidden>First name is required.</em>
  </label>
  <label class="field checkbox-group">
    <fieldset>
      <div class="choice">
        <input type="checkbox" value="travel" id="category-travel" name="category">
        <label for="category-travel">Travel</label>
      </div>
      <div class="choice">
        <input type="checkbox" value="marketing" id="category-marketing" name="category">
        <label for="category-marketing">Marketing</label>
      </div>
    </fieldset>
    <span class="label">Choose all categories that apply</span>
  </label>
</form>

Was ist dieses placeholder=" "? Es ist ein kleiner Trick, der es uns ermöglicht, in CSS zu verfolgen, ob das Feld leer ist, ohne JavaScript zu benötigen. Solange das Eingabefeld leer ist, entspricht es input:placeholder-shown, aber der Benutzer sieht keinen sichtbaren Platzhaltertext. Nur die Art von Dingen, die Sie tun können, wenn wir das Markup kontrollieren!

Jetzt, da wir wissen, was unser gewünschtes Ergebnis ist, hier ist der Spielplan

  1. Holen Sie sich die Felddefinitionen und andere Inhalte von der API
  2. Initialisieren Sie ein DOMDocument
  3. Iterieren Sie über die Felder und erstellen Sie jedes wie erforderlich
  4. Holen Sie sich die HTML-Ausgabe

Lassen Sie uns also unseren Prozess skizzieren und einige technische Details aus dem Weg räumen

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  $formResult = file_get_contents($endpoint);
  $formContent = json_decode($formResult);
  $formFields = $formContent->fields;

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // TODO: Do something with the field data
  }

  // Get the HTML output
  $dom->appendChild($form);
  $htmlString = $dom->saveHTML();
  echo $htmlString;
}

Bisher haben wir die Daten abgerufen und geparst, unser DOMDocument initialisiert und seine Ausgabe ausgegeben. Was wollen wir für jedes Feld tun? Zuerst erstellen wir das Container-Element, das in unserem Beispiel ein <label> sein sollte, und das beschriftende <span>, das für alle Feldtypen gemeinsam ist

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $label = null;

  // Add a `<span>` for the label if it is set
  if ($field->label) {
    $label = $dom->createElement('span', $field->label);
    $label->setAttribute('class', 'label');
  }

  // Add the label to the `<label>`
  if ($label) $element->appendChild($label);
}

Da wir uns in einer Schleife befinden und PHP Variablen in Schleifen nicht abschottet, setzen wir das $label-Element bei jeder Iteration zurück. Dann, wenn das Feld ein Label hat, erstellen wir das Element. Am Ende hängen wir es an das Container-Element an.

Beachten Sie, dass wir Klassen mit der Methode setAttribute festlegen. Im Gegensatz zur Web-API gibt es leider keine spezielle Behandlung von Klassenlisten. Sie sind einfach ein weiteres Attribut. Wenn wir wirklich komplexe Klassenlogik hätten, da es "It's Just PHP™" ist, könnten wir ein Array erstellen und es dann implodieren
$label->setAttribute('class', implode($labelClassList)).

Einzelne Eingaben

Da wir wissen, dass die API nur bestimmte Feldtypen zurückgibt, können wir nach Typ wechseln und spezifischen Code für jeden schreiben

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    case 'text':
    case 'email':
    case 'telephone':
      $input = $dom->createElement('input');
      $input->setAttribute('placeholder', ' ');
      if ($field->type === 'email') $input->setAttribute('type', 'email');
      if ($field->type === 'telephone') $input->setAttribute('type', 'tel');
      break;
  }
}

Behandeln wir nun Textbereiche, einzelne Kontrollkästchen und versteckte Felder

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'text_area':
      $input = $dom->createElement('textarea');
      $input->setAttribute('placeholder', ' ');
      if ($rows = $field->field_metadata->rows) $input->setAttribute('rows', $rows);
      break;

    case 'checkbox':
      $element->setAttribute('class', 'field single-checkbox');
      $input = $dom->createElement('input');
      $input->setAttribute('type', 'checkbox');
      if ($field->field_metadata->initially_checked === true) $input->setAttribute('checked', 'checked');
      break;

    case 'hidden':
      $input = $dom->createElement('input');
      $input->setAttribute('type', 'hidden');
      $input->setAttribute('value', $field->field_metadata->value);
      $element->setAttribute('hidden', 'hidden');
      $element->setAttribute('style', 'display: none;');
      $label->textContent = '';
      break;
  }
}

Beachten Sie etwas Neues, das wir für die Fälle mit Kontrollkästchen und versteckten Feldern tun? Wir erstellen nicht nur das <input>-Element; wir nehmen Änderungen am Container, dem <label>-Element, vor! Für ein einzelnes Kontrollkästchenfeld möchten wir die Klasse des Containers ändern, damit wir das Kontrollkästchen und das Label horizontal ausrichten können; der Container eines versteckten <input> sollte ebenfalls vollständig ausgeblendet werden.

Wenn wir nur Zeichenfolgen verketten würden, wäre es zu diesem Zeitpunkt unmöglich, Änderungen vorzunehmen. Wir müssten eine Reihe von if-Anweisungen bezüglich des Elementtyps und seiner Metadaten am Anfang des Blocks hinzufügen. Oder vielleicht schlimmer, wir beginnen den switch viel früher, kopieren dann viel gemeinsamen Code zwischen den einzelnen Zweigen.

Und hier liegt die wahre Schönheit der Verwendung eines Builders wie DOMDocument — bis wir saveHTML() erreichen, ist alles noch editierbar und alles ist noch strukturiert.

Verschachtelte Schleifenelemente

Fügen wir die Logik für <select>-Elemente hinzu

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'select':
      $element->setAttribute('class', 'field select');
      $input = $dom->createElement('select');
      $input->setAttribute('required', 'required');
      if ($field->field_metadata->multi_select === true)
        $input->setAttribute('multiple', 'multiple');
    
      $options = [];
    
      // Track whether there's a pre-selected option
      $optionSelected = false;
    
      foreach ($field->field_metadata->values as $value) {
        $option = $dom->createElement('option', htmlspecialchars($value->label));
    
        // Bail if there's no value
        if (!$value->value) continue;
    
        // Set pre-selected option
        if ($value->selected === true) {
          $option->setAttribute('selected', 'selected');
          $optionSelected = true;
        }
        $option->setAttribute('value', $value->value);
        $options[] = $option;
      }
    
      // If there is no pre-selected option, build an empty placeholder option
      if ($optionSelected === false) {
        $emptyOption = $dom->createElement('option');
    
        // Set option to hidden, disabled, and selected
        foreach (['hidden', 'disabled', 'selected'] as $attribute)
          $emptyOption->setAttribute($attribute, $attribute);
        $input->appendChild($emptyOption);
      }
    
      // Add options from array to `<select>`
      foreach ($options as $option) {
        $input->appendChild($option);
      }
  break;
  }
}

OK, hier passiert viel, aber die zugrundeliegende Logik ist dieselbe. Nach der Einrichtung des äußeren <select> erstellen wir ein Array von <option>s, die darin angehängt werden.

Wir machen hier auch einige <select>-spezifische Tricks: Wenn keine vordefinierte Option vorhanden ist, fügen wir eine leere Platzhalteroption hinzu, die bereits ausgewählt ist, aber vom Benutzer nicht ausgewählt werden kann. Das Ziel ist es, unser <label class="label"> per CSS als "Platzhalter" zu platzieren, aber diese Technik kann für alle Arten von Designs nützlich sein. Indem wir sie vor dem Anhängen der anderen Optionen an $input anhängen, stellen wir sicher, dass sie die erste Option im Markup ist.

Nun kümmern wir uns um <fieldset>s von Radio-Buttons und Checkboxen

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    // ...  
    case 'multiple_choice':
      $choiceType = $field->field_metadata->multi_select === true ? 'checkbox' : 'radio';
      $element->setAttribute('class', "field {$choiceType}-group");
      $input = $dom->createElement('fieldset');
    
      // Build a choice `<input>` for each option in the fieldset
      foreach ($field->field_metadata->values as $choiceValue) {
        $choiceField = $dom->createElement('div');
        $choiceField->setAttribute('class', 'choice');
    
        // Set a unique ID using the field ID + the choice ID
        $choiceID = "{$field->id}-{$choiceValue->value}";
    
        // Build the `<input>` element
        $choice = $dom->createElement('input');
        $choice->setAttribute('type', $choiceType);
        $choice->setAttribute('value', $choiceValue->value);
        $choice->setAttribute('id', $choiceID);
        $choice->setAttribute('name', $field->id);
        $choiceField->appendChild($choice);
    
        // Build the `<label>` element
        $choiceLabel = $dom->createElement('label', $choiceValue->label);
        $choiceLabel->setAttribute('for', $choiceID);
        $choiceField->appendChild($choiceLabel);
    
        $input->appendChild($choiceField);
      }
  break;
  }
}

Zuerst ermitteln wir, ob das Fieldset für Checkboxen oder Radio-Buttons sein soll. Dann setzen wir die Container-Klasse entsprechend und erstellen das <fieldset>. Danach iterieren wir über die verfügbaren Auswahlmöglichkeiten und erstellen für jede ein <div> mit einem <input> und einem <label>.

Beachten Sie, dass wir normale PHP-String-Interpolation verwenden, um die Container-Klasse in Zeile 21 festzulegen und eine eindeutige ID für jede Auswahl in Zeile 30 zu erstellen.

Fragmente

Der letzte Typ, den wir hinzufügen müssen, ist etwas komplexer, als er aussieht. Viele Formulare enthalten Anweisungsfelder, die keine Eingaben sind, sondern nur etwas HTML, das wir zwischen anderen Feldern drucken müssen.

Wir müssen uns auf eine weitere DOMDocument-Methode stützen, createDocumentFragment(). Dies ermöglicht es uns, beliebiges HTML hinzuzufügen, ohne die DOM-Strukturierung zu verwenden.

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // Build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Build the input element
  switch ($field->type) {
    //...  
    case 'instruction':
      $element->setAttribute('class', 'field text');
      $fragment = $dom->createDocumentFragment();
      $fragment->appendXML($field->text);
      $input = $dom->createElement('p');
      $input->appendChild($fragment);
      break;
  }
}

An diesem Punkt fragen Sie sich vielleicht, wie wir auf ein Objekt namens $input gestoßen sind, das tatsächlich ein statisches <p>-Element darstellt. Das Ziel ist es, einen gemeinsamen Variablennamen für jede Iteration der Feldschleife zu verwenden, damit wir ihn am Ende immer mit $element->appendChild($input) hinzufügen können, unabhängig vom tatsächlichen Feldtyp. Also ja, Dinge zu benennen ist schwer.

Validierung

Die API, die wir konsumieren, stellt dankenswerterweise eine individuelle Validierungsnachricht für jedes erforderliche Feld bereit. Wenn es einen Übermittlungsfehler gibt, können wir die Fehler inline zusammen mit den Feldern anzeigen, anstatt eine generische "Ups, Ihr Fehler"-Nachricht am Ende.

Fügen wir den Validierungstext zu jedem Element hinzu

<?php
// ...
// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // build the container `<label>`
  $element = $dom->createElement('label');
  $element->setAttribute('class', 'field');

  // Reset input values
  $input = null;
  $label = null;
  $validation = null;

  // Add a `<span>` for the label if it is set
  // ...

  // Add a `<em>` for the validation message if it is set
  if (isset($field->validation_message)) {
    $validation = $dom->createElement('em');
    $fragment = $dom->createDocumentFragment();
    $fragment->appendXML($field->validation_message);
    $validation->appendChild($fragment);
    $validation->setAttribute('class', 'validation-message');
    $validation->setAttribute('hidden', 'hidden'); // Initially hidden, and will be unhidden with Javascript if there's an error on the field
  }

  // Build the input element
  switch ($field->type) {
    // ...
  }
}

Das ist alles, was es braucht! Kein Herumfummeln mit der Feldtyp-Logik erforderlich — erstellen Sie einfach bedingt ein Element für jedes Feld.

Alles zusammenbringen

Was passiert also, nachdem wir alle Feldelemente erstellt haben? Wir müssen die Objekte $input, $label und $validation zum DOM-Baum hinzufügen, den wir erstellen. Wir können die Gelegenheit auch nutzen, um gemeinsame Attribute wie required hinzuzufügen. Dann fügen wir die Submit-Schaltfläche hinzu, die in dieser API von den Feldern getrennt ist.

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  // ...

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // Build the container `<label>`
    $element = $dom->createElement('label');
    $element->setAttribute('class', 'field');
  
    // Reset input values
    $input = null;
    $label = null;
    $validation = null;
  
    // Add a `<span>` for the label if it is set
    // ...
  
    // Add a `<em>` for the validation message if it is set
    // ...
  
    // Build the input element
    switch ($field->type) {
      // ...
    }
  
    // Add the input element
    if ($input) {
      $input->setAttribute('id', $field->id);
      if ($field->required)
        $input->setAttribute('required', 'required');
      if (isset($field->max_length))
        $input->setAttribute('maxlength', $field->max_length);
      $element->appendChild($input);
  
      if ($label)
        $element->appendChild($label);
  
      if ($validation)
        $element->appendChild($validation);
  
      $form->appendChild($element);
    }
  }
  
  // Build the submit button
  $submitButtonLabel = $formContent->submit_button_label;
  $submitButtonField = $dom->createElement('div');
  $submitButtonField->setAttribute('class', 'field submit');
  $submitButton = $dom->createElement('button', $submitButtonLabel);
  $submitButtonField->appendChild($submitButton);
  $form->appendChild($submitButtonField);

  // Get the HTML output
  $dom->appendChild($form);
  $htmlString = $dom->saveHTML();
  echo $htmlString;
}

Warum prüfen wir, ob $input wahr ist? Da wir es am Anfang der Schleife auf null zurücksetzen und es nur erstellen, wenn der Typ unseren erwarteten Switch-Fällen entspricht, stellen wir sicher, dass wir keine unerwarteten Elemente aufnehmen, die unser Code nicht richtig verarbeiten kann.

Und siehe da, ein benutzerdefiniertes HTML-Formular!

Bonuspunkte: Zeilen und Spalten

Wie Sie vielleicht wissen, erlauben viele Formular-Builder Autoren, Zeilen und Spalten für Felder festzulegen. Zum Beispiel könnte eine Zeile sowohl das Feld für den Vornamen als auch den Nachnamen enthalten, jeweils in einer einzelnen Spalte mit 50% Breite. Wie würden wir das also implementieren, fragen Sie? Indem wir (noch einmal) beispielhaft zeigen, wie schleifenfreundlich DOMDocument ist, natürlich!

Unsere API-Antwort enthält die Grid-Daten wie folgt

{
  "submit_button_label": "Submit now!",
  "fields": [
    {
      "id": "first-name",
      "type": "text",
      "label": "First name",
      "required": true,
      "validation_message": "First name is required.",
      "max_length": 30,
      "row": 1,
      "column": 1
    },
    {
      "id": "category",
      "type": "multiple_choice",
      "label": "Choose all categories that apply",
      "required": false,
      "field_metadata": {
        "multi_select": true,
        "values": [
          { "value": "travel", "label": "Travel" },
          { "value": "marketing", "label": "Marketing" }
        ]
      },
      "row": 2,
      "column": 1
    }
  ]
}

Wir gehen davon aus, dass das Hinzufügen eines Attributs data-column für das Styling der Breite ausreicht, aber jede Zeile muss ihr eigenes Element sein (d. h. kein CSS-Grid).

Bevor wir uns damit befassen, denken wir darüber nach, was wir brauchen, um Zeilen hinzuzufügen. Die grundlegende Logik sieht etwa so aus

  1. Verfolgen Sie die zuletzt angetroffene Zeile.
  2. Wenn die aktuelle Zeile größer ist, d. h. wir sind zur nächsten Zeile gesprungen, erstellen Sie ein neues Zeilenelement und beginnen Sie, stattdessen dorthin statt in das vorherige Element hinzuzufügen.

Wie würden wir das machen, wenn wir Zeichenfolgen verketten würden? Wahrscheinlich, indem wir eine Zeichenfolge wie '</div><div class="row">' hinzufügen, wann immer wir eine neue Zeile erreichen. Diese Art von "umgekehrtem HTML-String" ist für mich immer sehr verwirrend, daher kann ich nur erraten, wie mein IDE sich fühlt. Und das Tüpfelchen auf dem i ist, dass dank des automatischen Schließens offener Tags durch den Browser ein einziger Tippfehler zu gazillionen verschachtelten <div>s führt. Genau wie Spaß, nur das Gegenteil.

Was ist also der strukturierte Weg, dies zu handhaben? Danke der Nachfrage. Zuerst fügen wir die Zeilenverfolgung vor unserer Schleife hinzu und erstellen ein zusätzliches Zeilen-Container-Element. Dann stellen wir sicher, dass jedes Container-$element an sein $rowElement angehängt wird, anstatt direkt an $form.

<?php
function renderForm ($endpoint) {
  // Get the data from the API and convert it to a PHP object
  // ...

  // Start building the DOM
  $dom = new DOMDocument();
  $form = $dom->createElement('form');

  // init tracking of rows
  $row = 0;
  $rowElement = $dom->createElement('div');
  $rowElement->setAttribute('class', 'field-row');

  // Iterate over the fields and build each one
  foreach ($formFields as $field) {
    // Build the container `<label>`
    $element = $dom->createElement('label');
    $element->setAttribute('class', 'field');
    $element->setAttribute('data-row', $field->row);
    $element->setAttribute('data-column', $field->column);
    
    // Add the input element to the row
    if ($input) {
      // ...
      $rowElement->appendChild($element);
      $form->appendChild($rowElement);
    }
  }
  // ...
}

Bisher haben wir nur ein weiteres <div> um die Felder herum hinzugefügt. Erstellen wir für jede Zeile innerhalb der Schleife ein neues Zeilenelement

<?php
// ...
// Init tracking of rows
$row = 0;
$rowElement = $dom->createElement('div');
$rowElement->setAttribute('class', 'field-row');

// Iterate over the fields and build each one
foreach ($formFields as $field) {
  // ...
  // If we've reached a new row, create a new $rowElement
  if ($field->row > $row) {
    $row = $field->row;
    $rowElement = $dom->createElement('div');
    $rowElement->setAttribute('class', 'field-row');
  }

  // Build the input element
  switch ($field->type) {
    // ...  
    // Add the input element to the row
      if ($input) {
        // ...
        $rowElement->appendChild($element);

        // Automatically de-duped
        $form->appendChild($rowElement);
      }
  }
}

Alles, was wir tun müssen, ist, das $rowElement-Objekt als neues DOM-Element zu überschreiben, und PHP behandelt es als ein neues, eindeutiges Objekt. Am Ende jeder Schleife hängen wir also einfach an, was das aktuelle $rowElement ist — wenn es dasselbe wie in der vorherigen Iteration ist, wird das Formular aktualisiert; wenn es ein neues Element ist, wird es am Ende angehängt.

Wo geht es von hier aus weiter?

Formulare sind ein großartiger Anwendungsfall für objektorientiertes Templating. Und wenn man an diesen Ausschnitt aus dem WordPress Core denkt, könnte man argumentieren, dass verschachtelte Menüs ebenfalls ein guter Anwendungsfall sind. Jede Aufgabe, bei der das Markup einer komplexen Logik folgt, ist ein guter Kandidat für diesen Ansatz. DOMDocument kann jedes XML ausgeben, sodass Sie es auch verwenden könnten, um einen RSS-Feed aus Post-Daten zu erstellen.

Hier ist der gesamte Codeausschnitt für unser Formular. Passen Sie ihn gerne an jede Formular-API an, mit der Sie es zu tun haben. Hier ist die offizielle Dokumentation, die gut geeignet ist, um einen Eindruck von der verfügbaren API zu bekommen.

Wir haben nicht einmal erwähnt, dass DOMDocument bestehendes HTML und XML parsen kann. Sie können dann Elemente über die XPath-API nachschlagen, die irgendwie document.querySelector oder cheerio in Node.js ähnelt. Es gibt eine gewisse Lernkurve, aber es ist eine super mächtige API für die Verarbeitung externer Inhalte.

Spaß(?) -Fakt: Microsoft Office-Dateien, die mit x enden (z. B. .xlsx), sind XML-Dateien. Sagen Sie es nicht der Marketingabteilung, aber es ist möglich, Word-Dokumente zu parsen und HTML auf dem Server auszugeben.

Das Wichtigste ist, sich daran zu erinnern, dass Templating eine Superkraft ist. Die Fähigkeit, das richtige Markup für die richtige Situation zu erstellen, kann der Schlüssel zu einer großartigen UX sein.