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
- Holen Sie sich die Felddefinitionen und andere Inhalte von der API
- Initialisieren Sie ein
DOMDocument - Iterieren Sie über die Felder und erstellen Sie jedes wie erforderlich
- 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
- Verfolgen Sie die zuletzt angetroffene Zeile.
- 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.
Sehr inspirierender Artikel, und es ist immer toll, über PHP zu lesen. Aber ehrlich gesagt, ich bin nicht überzeugt und denke immer noch, dass in den meisten Fällen die Arbeit mit Vorlagen-Strings in PHP (oder JS!) einfacher zu schreiben und zu lesen ist. Mit (s)printf, (nummerierten) Platzhaltern, ternären Operatoren (plus deren Kurzformen wie ?? und ?:) und gut formatiertem Code kann man mit weniger Code viel erreichen, und es ist leichter zu sehen, wie die endgültige HTML-Ausgabe aussieht. Aber ja, für wirklich komplexe Aufgaben mit vielen Variationen ist es gut, sich daran zu erinnern, dass DOMDocument existiert!
Toller Artikel! Ich wollte mitteilen, dass ich https://GitHub.com/PhpGt/Dom pflege, das auf DOMDocument aufbaut, um all die schönen modernen Dinge hinzuzufügen, die wir von der clientseitigen Entwicklung erwarten. Hoffentlich hilft es jemandem.
Wow, das sieht wirklich gut aus. Ich werde das beim nächsten Mal auf jeden Fall ausprobieren. Bonuspunkte für das ausgezeichnete Anti-Beispiel im Nutzungsabschnitt :)
Ihr Schreibstil ist sehr angenehm! Ich habe monatelang darüber nachgedacht, etwas Ähnliches zu bauen, und Sie haben mir mit diesem Artikel eine neue Perspektive auf dieses Thema gegeben. Danke!
Toller Artikel zur Erklärung der OO-HTML-Generierung. Es ist der richtige Weg. Vorlagen und Include-Dateien sind so 1950er Jahre. FORTRAN benutzte Include-Dateien, wir müssen das nicht.
Ich habe einen etwas anderen Ansatz gewählt. Ich wollte die Arbeit an der Erstellung responsiver Seiten automatisieren und den gesamten Boilerplate-Code entfernen. Außerdem wollte ich einen höheren Ansatz als den, den Sie hier skizziert haben. Deshalb habe ich eine PHP OO-Bibliothek um Foundation herum geschrieben. Im Grunde erstellen Sie eine Seite, die alles Notwendige für eine vollständige Foundation-Seite behandelt, einschließlich aller JS usw., dann fügen Sie Dinge hinzu. Sie können einfachen Text oder andere Objekte hinzufügen, die in Zeichenfolgen konvertiert werden können. Objekte können ein Akkordeon-Menü oder eine Callout-Box oder was auch immer Sie erstellen möchten sein.
Im Allgemeinen habe ich für meine App ein Standard-Page-Objekt, das alle Menüs, Footer, Header usw. enthält, dann füge ich spezifische View-Objekte hinzu, z. B. ein Bearbeitungsformular oder eine Tabellenansicht.
Die Seite mit echo ausgeben, und ich bin fertig. Einfach, erweiterbar, typsicher und vollständig gültiges HTML. Schauen Sie sich etwas wie eine Liste von-bis an. Es gibt einen Quellcode-Tab, der alles zeigt, was benötigt wird, um die Seite in 100% PHP zu generieren. Alle anderen Arbeiten wie das Einbinden von JavaScript-Bibliotheken, CSS usw. werden für Sie erledigt.
Ich habe die Website auch in meinem PHP-Dokumentationswerkzeug geschrieben, so dass Sie vollständige Einblicke in die Klassenstruktur, den Dateiquellcode und sogar die Git-Historie haben (für dieses Projekt, die eigentliche Website, nicht die individuellen Bibliotheks-Historien).
Schauen Sie es sich an. Die Zukunft des Webdesigns.