20.5.2. Nutzung der REST-API mit dem API-Fetch Paket

In WordPress 5.0 hielt ein neuer Editor (Codename: Gutenberg) Einzug in das Content Management System. Der neue Editor, der komplett auf Blöcken basiert wurde fast ausschließlich in JavaScript geschrieben. Dazu gab es mehrere JavaScript-Pakete, die nun in WordPress integriert wurden. Eine davon nennt sich API-Fetch und genau um diese geht es hier.

Hinweise

  • apiFetch ist ein Gerüst, welches um window.fetch aufgebaut wurde. Das bedeutet, es funktioniert letztlich genauso wie der offizielle Pendant, mit dem Unterschied, dass es auf WordPress zugeschnitten wurde. So wird zum Beispiel automatisch vorausgesetzt, dass sich die REST-API unter https://ihre-domain/wp-json/ befindet. Dazu kommt, dass Sie sich nicht um Nonces kümmern müssen.
  • Dieses Paket setzt voraus, dass Ihr Code in einer ES2015+-Umgebung ausgeführt wird. Wenn Sie eine Umgebung verwenden, die nur begrenzte oder gar keine Unterstützung für ES2015+ bietet, wie z.B. niedrigere Versionen des Internet Explorer, dann wird die Verwendung von core-js oder @babel/polyfill die Unterstützung für diese Methoden hinzufügen. Erfahren Sie mehr darüber in der Babel-Dokumentation.
  • Beachten Sie, dass wir im Beispiel die Funktion querySelector() nutzen werden. Sie ist in älteren Versionen des Internet Explorer (<= 8) möglicherweise nicht vorhanden. Ähnliches gilt für die Klasse FormData, die ursprünglich in den „XMLHttpRequest Advanced Features“ spezifiziert wurden.1

Im vorherigen Kapitel haben wir über die Backbone.js Bibliothek Daten dynamisch nachgeladen und eine Paginierung eingeführt. Nun wollen wir durch Zuhilfenahme des API-Fetch Pakets neue Daten hinzufügen.

Wir bauen also auf den ursprünglichen Code (siehe Github) auf und erweitern unser Plugin wie folgt:

API-Fetch integrieren

Zuerst erweitern wir in der Datei inc/backend.php folgende Zeilen:

Dieser Inhalt ist nur eingeschränkt verfügbar. Melden Sie sich vorher an.

Wir machen unser Script also zusätzlich abhängig von wp-api-fetch. Damit wird sichergestellt, dass das Script auch tatsächlich geladen wird, wenn wir es benötigen. Das Hinzufügen dieses Strings gibt uns dann auch sofort Zugriff auf die globale Variable wp.apiFetch, die wir per JavaScript ansprechen können.

Formulardaten per Ajax senden

In der js/backend.js-Datei ergänzen wir in der JavaScript-Klasse outgoing_links_class folgende Methode:

Dieser Inhalt ist nur eingeschränkt verfügbar. Melden Sie sich vorher an.

Danach rufen wir sie über

Dieser Inhalt ist nur eingeschränkt verfügbar. Melden Sie sich vorher an.

auf. Sie sorgt dafür, dass beim Klicken des Absende-Buttons ein Link entweder hinzugefügt oder aber aktualisiert wird. Wir übergeben dabei die Formulardaten als einzigen Parameter.

Daten an die REST-API senden

Wir unterscheiden also:

this.add_link = function ( form_data ) {} );
this.update_link = function ( form_data ) {} );

Die erste davon wollen wir nun mit Inhalt füllen:

Dieser Inhalt ist nur eingeschränkt verfügbar. Melden Sie sich vorher an.

Die Formulardaten werden an den an den Endpunkt POST /tel/v1/outgoing-links/ gesendet.

Die Zeile form_data.delete( '_wpnonce' ); ist nötig, weil wir eine anderen Nonce benötigen als die, die im Formular definiert wurde. Immerhin greifen wir ja direkt auf die REST-API zu. Und die richtige Nonce ist bereits in apiFetch integriert. (Genauer gesagt wird sie beim Aufruf der Seite integriert. Wenn Sie sich den Quellcode der Seite ansehen, werden Sie so etwas finden: wp.apiFetch.use( wp.apiFetch.createNonceMiddleware( "e9fda8747e" ) );.)

Nun führt die Funktion eine asynchrone Abfrage durch. Wenn der Browser damit fertig ist, wird er then() im Erfolgs- oder catch() im Fehlerfall ansteuern.

Im catch-Block geben wir den Fehler einmal via Konsole und im Hinweisfenster aus.

Im Fehlerfall die richtige Übersetzung

Damit wir auch die entsprechende Übersetzung erhalten, ergänzen wir bei unseren Konstanten (siehe Datei inc/backend.php):

<?php
$args = array(
	'total_records' => Link_Table::record_count(),
	'i18n'          => [
		'an_error_occurred' => __( 'An error occurred: %s', 'track-external-links' ),
	],
);

wp_add_inline_script(
	'track-external-links',
	sprintf( 'var tel_script_constants = %s;', json_encode( (object) $args ) ),
	'before'
);
?>

%s dient dabei als Platzhalter für die eigentliche Fehlermeldung.

Der Erfolgsfall

Im Erfolgsfall wollen wir eine neue Tabellenzeile einfügen. Wir benötigen also noch die Funktion add_link_to_table(), wie weiter oben angegeben.

Der Großteil ist Fleißarbeit, denn in JavaScript muss jedes HTML-Element einzeln erstellt und in die DOM eingehängt werden.

this.add_link_to_table = function ( link ) {
  var table = document.getElementById( 'the-list' );
  var row   = table.insertRow( 0 );

  /* id field */
  var cell_id       = row.insertCell( 0 );
  var cell_id_input = function ( link_id ) {
    var child = document.createElement( 'input' );
    child.setAttribute( 'type', 'checkbox' );
    child.setAttribute( 'name', 'bulk-delete[]' );
    child.setAttribute( 'value', link_id );
    return child;
  }( link.id );
  cell_id.appendChild( cell_id_input );

  /* title field */
  var cell_title      = row.insertCell( 1 );
  var cell_title_html = function ( link_url, link_title ) {
    var child = document.createElement( 'a' );
    child.setAttribute( 'href', link_url );
    child.setAttribute( 'target', '_blank' );
    child.innerText = link_title;
    return child;
  }( link.link, link.title );
  cell_title.appendChild( cell_title_html );
  var cell_title_actions = function ( i18n, link_id, delete_link ) {
    var div = document.createElement( 'div' );
    div.setAttribute( 'class', 'row-actions' );

    /* span1 */
    var span1 = document.createElement( 'span' );
    span1.setAttribute( 'class', 'edit' );

    /* link1 */
    var link1 = document.createElement( 'a' );
    link1.setAttribute( 'href', '#' );
    link1.setAttribute( 'data-id', link_id );
    link1.addEventListener( 'click', edit_link );
    link1.innerText = i18n.edit;

    span1.appendChild( link1 );
    span1.appendChild( document.createTextNode( ' | ' ) );
    div.appendChild( span1 );

    /* span 2 */
    var span2 = document.createElement( 'span' );
    span2.setAttribute( 'class', 'delete' );

    /* link 2 */
    var link2 = document.createElement( 'a' );
    link2.setAttribute( 'href', delete_link.replace( '##id##', link_id ) );
    link2.innerText = i18n.delete;

    span2.appendChild( link2 );
    div.appendChild( span2 );


    return div;
  }( this.constants.i18n, link.id, this.constants.delete_link );
  cell_title.appendChild( cell_title_actions );

  /* outgoing link field */
  var cell_outoing       = row.insertCell( 2 );
  var cell_outgoing_html = function ( link_url ) {
    var child = document.createElement( 'a' );
    child.setAttribute( 'href', link_url );
    child.setAttribute( 'target', '_blank' );
    child.innerText = link_url;
    return child;
  }( this.constants.site_url + 'out/' + link.slug  );
  cell_outoing.appendChild( cell_outgoing_html );

  /* going-to field */
  var cell_goingto      = row.insertCell( 3 );
  var cell_goingto_html = function ( link_url ) {
    var child = document.createElement( 'a' );
    child.setAttribute( 'href', link_url );
    child.setAttribute( 'target', '_blank' );
    child.innerText = link_url;
    return child;
  }( link.link );
  cell_goingto.appendChild( cell_goingto_html );

  /* count field */
  var cell_count = row.insertCell( 4 );
  cell_count.appendChild( document.createTextNode( link.count ) );
};

Soweit so gut. Was noch fehlt sind die Konstanten, die wir hier zusätzlich benötigen. Wir ergänzen in der /inc/backend.php ein paar Übersetzungen sowie die Seiten-URL und den Link zum Löschen eines Links:

<?php
$args = array(
	'total_records' => Link_Table::record_count(),
	'site_url'      => trailingslashit( site_url() ),
	'i18n'          => [
		'an_error_occurred' => __( 'An error occurred: %s', 'track-external-links' ),
		'edit'              => __( 'Edit' ),
		'delete'            => __( 'Delete' ),
	],
	'delete_link'   => add_query_arg( array(
		'page'     => esc_attr( $_REQUEST['page'] ),
		'action'   => 'delete',
		'id'       => '##id##',
		'_wpnonce' => wp_create_nonce( 'f/tel/link/delete' ),
	) )
);

wp_add_inline_script(
	'track-external-links',
	sprintf( 'var tel_script_constants = %s;', json_encode( (object) $args ) ),
	'before'
);
?>

Hinweis

Beachten Sie, dass die Übersetzung für Edit und Delete keine Textdomain-Angabe enthalten. Das ist absichtlich so. Denn damit kommt die Übersetzung direkt aus der Übersetzungsdatei von WordPress selbst und wir müssen uns nicht darum kümmern, diese einfachen Wörter selbst zu übersetzen.

Machen wir uns an die Update-Funktion:

Ein Link-Update

this.update_link = function ( form_data ) {
  var self = this;

  /* We need the REST-API nonce, not the form-nonce. */
  form_data.delete( '_wpnonce' );

  var id = form_data.get( 'id' );
  form_data.delete( 'id' );

  wp.apiFetch( {
       'path':   '/tel/v1/outgoing-links/' + id,
       'method': 'POST',
       'body':   form_data,
     } ).
     then( function ( data ) {
       self.update_link_in_table( data );
     } )
     .catch( function ( error ) {
      console.error( error );
      alert( self.constants.i18n.an_error_occurred.replace( '%s', error.message ) );
  } );

};

Wir senden an den Endpunkt POST /tel/v1/outgoing-links/<id>. Wobei <id> die aktuelle Link-ID ist. Wie Sie sehen, bekommen wir diese aus den Formulardaten. Danach löschen wir die ID aus form_data weil wir sie nicht doppelt senden müssen.

Auch hier gilt: die _wpnonce muss aus den Formulardaten gelöscht werden bevor wir fortfahren können.

Der Update-Fehlerfall …

wird genauso gehandhabt wie beim Erstellen eines Links.

Der Erfolgsfall

Im Erfolgsfall müssen wir den jeweiligen Eintrag in der Tabelle aktualisieren, falls dieser sichtbar ist. Auch hier gibt es viel Fleißarbeit:

this.update_link_in_table = function ( link ) {
  var checkbox = document.querySelector(
      '#the-list input[type="checkbox"][value="' + link.id + '"]',
  );

  if ( checkbox.length <= 1 ) {
    return;
  }

  var row = checkbox.parentNode.parentNode;

  /* the title */
  var link_title = row.querySelector( 'td:nth-child(2) a' );
  link_title.setAttribute( 'href', link.link );
  link_title.innerText = link.title;

  /* outgoing link */
  var outgoing_link = row.querySelector( 'td:nth-child(3) a' );
  outgoing_link.setAttribute( 'href', this.constants.site_url + 'out/' + link.slug );
  outgoing_link.innerText = this.constants.site_url + 'out/' + link.slug;

  /* going to */
  var goingto_link = row.querySelector( 'td:nth-child(4) a' );
  goingto_link.setAttribute( 'href', link.link );
  goingto_link.innerText = link.link;

  /* count */
  var count       = row.querySelector( 'td:nth-child(5)' );
  count.innerText = link.count;

};

Fertig. Sie können nun einzelne Links per JavaScript hinzufügen und aktualisieren. Probieren Sie es aus. Den gesamten Code finden Sie – wie immer – auf Github.

Im nächsten Kapitel wollen wir die REST-API über jQuery ansteuern und Links löschen.

  1. XMLHttpRequest Advanced Features: https://www.w3.org/TR/2012/WD-XMLHttpRequest-20120117/#interface-formdata ↩︎