Ultimamente ho avuto a che fare con il backend di WordPress per l’implementazione di diversi plugin che, forniti ai nostri clienti, permettono di avere comode liste di dati ottenute dall’esterno all’interno dell’interfaccia di amministrazione.

Nel mio caso specifico l’esigenza era: raccogliere dei datasets da servizi esterni mediante architettura REST e le API fornite dai servizi e mostrarli all’interno di sezioni specifiche del plugin consentendo di applicare filtri sui dati, ordinamento e ricerca.

Una comoda soluzione integrata nel Core di WordPress per gestire liste dati

Non ci sofferemiamo sulla provenienza del nostro set di dati che possiamo ricavare tramite una query al database, un array statico oppure come nel mio caso da API esterne. Questa infatti è una scelta dettata dall’architettura della nostra applicazione e del tutto ininfluente ai fini del mio studio.

Lavorando sul backend ho ripiegato subito sulla classe nativa WP_List_Table che permette attraverso i metodi che affronteremo nel dettaglio tra poco di organizzare il nostro dataset e restituirlo in una tabella html.

Se non conosci WP_List_Table, sappi che per esempio la lista degli Articoli e delle Pagine di WordPress è ottenuta attraverso una classe che ne estende le funzionalità.

Essendo infatti privata la visibilità della classe WP_List_Table, per poter usufruire dei suoi vantaggi e delle sue funzionalità dovremo creare una classe che la estende e procedere, qualora sia necessario, con un override dei metodi forniti.

  1.     	class My_List_Table extends WP_List_Table {
  2.  
  3. 	}

Definizione dei metodi

Costruttore

Il primo override lo facciamo già sul costruttore di classe, indicandogli 3 parametri:

  1. singular, stringa che specifica il nome singolare della nostra lista dati
  2. plural, stringa che specifica il nome plurale della nostra lista dati
  3. ajax, booleano che specifica se i dati vengono ottenuti tramite ajax o meno.

Nel mio caso l’esigenza era quella di ottenere i dati tramite ajax in quanto reperendoli dall’esterno i tempi di caricamento potevano essere variabili e non lineari. Non utilizzando questa soluzione ovviamente funzionerebbe tutto lo stesso ma la classe entra in una modalità bloccante che mette in pausa il caricamento della pagina fino a quando non sono stati ottenuti i dati.

Specificando come modalità ajax e caricando i dati in maniera asincrona tramite una chiamata Javascript, possiamo durante l’attesa mostrare la pagina ed un caricamento. Attraverso una chiamata al metodo privato _js_vars() vengono impostate tutte le variabili necessarie per effettuare la chiamata e scaricare i dati.

  1.  
  2. 	function __construct() {
  3.  
  4. 		parent::__construct(
  5. 			array(
  6. 				'singular'  => 'dato',
  7. 				'plural'    => 'dati',
  8. 				'ajax'      => true
  9. 			)
  10. 		);
  11.  
  12.  
  13. 	}

Stabiliamo le colonne della nostra tabella usando il metodo get_columns e column_default

Da quali colonne sarà formata la nostra tabella?

Per fare ciò effettuiamo l’override del metodo get_columns che deve restituire un array associativo in cui le chiavi rappresentano lo slug e i valori il nome che assegnamo alle colonne della tabella.

  1.  
  2. 	function get_columns() {
  3.  
  4. 		  $columns = array(
  5. 		    'slug'        => 'Campo1',
  6. 		    'slug_2'  => 'Campo2',
  7. 		  );
  8. 		  return $columns;
  9.  
  10. 	}

Con un override del metodo column_default possiamo definire come renderizzare i dati che vengono posizionati nelle rispettive colonne.

  1.  
  2. 	function column_default( $item, $column_name ) {
  3. 		switch( $column_name ) {
  4. 			case 'slug':
  5. 				return "Colonna :".$item[ $column_name ];
  6. 			default:
  7. 				  return $item[ $column_name ];
  8. 		}
  9. 	}

Per esempio la colonna slug verrà renderizzata preceduta dalla stringa Colonna, mentre per tutte le altre colonne verrà semplicemente mostrato il valore corrispondente.

Prepariamo i dati da mostrare nella tabella attraverso il metodo prepare_items

Con un override del metodo prepare_items impostiamo le intestazione ed il contenuto della tabella.

  1. 	function prepare_items() {
  2. 		  $columns = $this->get_columns();
  3. 		  $hidden = array("slug");
  4. 		  $sortable = array();		  
  5. 		  $this->_column_headers = array($columns, $hidden, $sortable);
  6.  
  7. 	      $this->items = $items;
  8.  
  9. 		  $this->set_pagination_args( array(
  10. 		    'total_items' => $total_items, 
  11. 		    'per_page'    => $per_page 
  12. 		  ) );
  13. 	}

La variabile di classe _column_headers è un array che contiene:

  1. un array contenente gli slug delle colonne della tabella che richiamiamo con il metodo get_columns
  2. un array contenente gli slug delle colonne della tabella che non vogliono essere mostrate
  3. un array contenente gli slug delle colonne della tabella che vogliamo ordinare

Inoltre impostiamo la variabile di classe items con i dati da mostrare, possiamo per esempio impostarne il valore come una funzione che ricava i dati dall’esterno come nel mio caso. Il formato di dati dovrà sempre essere un array associativo le cui chiavi sono gli slug e il valore sono il contenuto delle colonne.

Quanti elementi mostriamo in totale? Quanti per pagina? E quante sono le pagine in totale? Abbiamo bisogno di specificare tutti i parametri necessari per la paginazione dei risultati nella nostra tabella.

Attraverso il metodo set_pagination_args specifichiamo tutti questi dati che calcoliamo noi secondo la logica che desideriamo impostare e il set di dati in ingresso.

Mostriamo i dati con il metodo display

Il metodo display costruisce la struttura dati andando a creare ogni riga della tabella. Possiamo effettuare un override nel modo seguente.

  1. 	function display() {
  2. 		wp_nonce_field( 'ajax-custom-list-nonce', '_ajax_custom_list_nonce' );
  3. 		parent::display();
  4. 	}

In pratica richiamiamo il metodo display e generiamo un valore (denominato nonce) che inseriamo in un field mediante la funzione wp_nonce_field. Questo valore servirà per validare la nostra richiesta, tra poco vedremo come.

…e finalmente AJAX!

Premessa: perchè non ho trovato nulla che mi ha convinto in rete…

Siamo arrivati al punto in cui abbiamo impostato la nostra classe ereditata da WP_List_Table, l’ultimo passo è quello di impostare ancora il metodo ajax_response e richiamare il tutto tramite una chiamata asincrona in Javascript.

Qui però faccio una piccola deviazione rispetto a quello che potete trovare in merito su internet e vi spiego perché.

Diversi how-to che trovate in rete caricano la prima pagina senza ricorrere ad ajax, lasciando che solamente le successive vadano a popolare la tabella attraverso chiamate asincrone. In questa maniera la prima pagina carica la struttura della tabella e il contenuto relativo, ciò porta al fatto che inizialmente la pagina è bloccata aspettando che il caricamento finisca.

Questa soluzione non mi piace: nel mio caso l’architettura REST esterna porta ad un’imprevedibilità sui tempi di caricamento della pagina come ho spiegato all’inizio dell’articolo.

… E come mi sono arrangiato

Il metodo ajax_response

Il metodo ajax_response che inseriamo all’interno della classe che stiamo costruendo serve per restituire i dati nel formato di interscambio JSON che andremo a richiamare tramite Javascript ed è relativo alle righe della nostra tabella.

  1. 	function ajax_response() {
  2.  
  3. 		check_ajax_referer( 'ajax-custom-list-nonce', '_ajax_custom_list_nonce' );
  4.  
  5. 		$this->prepare_items();
  6.  
  7. 		extract( $this->_args );
  8. 		extract( $this->_pagination_args, EXTR_SKIP );
  9.  
  10. 		ob_start();
  11. 		if ( ! empty( $_REQUEST['no_placeholder'] ) )
  12. 			$this->display_rows();
  13. 		else
  14. 			$this->display_rows_or_placeholder();
  15. 		$rows = ob_get_clean();
  16.  
  17. 		ob_start();
  18. 		$this->print_column_headers();
  19. 		$headers = ob_get_clean();
  20.  
  21. 		ob_start();
  22. 		$this->pagination('top');
  23. 		$pagination_top = ob_get_clean();
  24.  
  25. 		ob_start();
  26. 		$this->pagination('bottom');
  27. 		$pagination_bottom = ob_get_clean();
  28.  
  29. 		$response = array( 'rows' => $rows );
  30. 		$response['pagination']['top'] = $pagination_top;
  31. 		$response['pagination']['bottom'] = $pagination_bottom;
  32. 		$response['column_headers'] = $headers;
  33.  
  34. 		if ( isset( $total_items ) )
  35. 			$response['total_items_i18n'] = sprintf( _n( '1 item', '%s items', $total_items ), number_format_i18n( $total_items ) );
  36.  
  37. 		if ( isset( $total_pages ) ) {
  38. 			$response['total_pages'] = $total_pages;
  39. 			$response['total_pages_i18n'] = number_format_i18n( $total_pages );
  40. 		}
  41.  
  42. 		die( json_encode( $response ) );
  43. 	}

Una nota che mi sento di fare riguardo questo metodo è la chiamata check_ajax_referer che valida il codice nonce che abbiamo inserito nel metodo display e impedisce la restituzione dei dati nel caso sia diverso .

Richiamiamo la tabella nella sezione del nostro plugin

Quello che dobbiamo fare ora e andare a richiamare la tabella nella sezione del nostro plugin ed è qui che ho invertito rotta rispetto a quanto trovato in giro.

Volendo caricare tutto tramite AJAX la nostra pagina sarà piuttosto vuota e conterrà solamente gli elementi HTML in cui verrà inserita la tabella.

Un esempio di come può essere strutturata è quello che segue.

  1. 	<form id="email-sent-list" method="get">
  2.  
  3. 		<input type="hidden" name="page" value="<?php echo $_REQUEST['page'] ?>" />
  4.  
  5. 		<div id="ts-history-table" style="">
  6. 			<?php
  7. 				wp_nonce_field( 'ajax-custom-list-nonce', '_ajax_custom_list_nonce' );
  8. 			?>
  9. 		</div>
  10.  
  11. 	</form>

Predisponiamo un form a cui assegniamo un ID (nel mio caso email-sent-list), all’interno del form ci saranno:

  • un input hidden denominato page che ha come valore il numero della pagina
  • un contenitore div assegnato ad un ID (nel mio caso ts-history-table)
  • la funzione wp_nonce_field all’interno del contenitore div che sarà esattamente la stessa che è contenuta nel metodo display e che serve per validare la prima pagina

Due hooks per la gestione delle callback AJAX

Andiamo a prepare due hooks di WordPress per la gestione delle callback Ajax.

  1. 	function _ajax_fetch_ts_history_callback() {
  2.  
  3. 		$wp_list_table = new My_List_Table();
  4. 		$wp_list_table->ajax_response();
  5. 	}
  6.  
  7. 	add_action( 'wp_ajax__ajax_fetch_ts_history', '_ajax_fetch_ts_history_callback' );
  8.  
  9. 	function _ajax_ts_display_callback() {
  10.  
  11. 		check_ajax_referer( 'ajax-custom-list-nonce', '_ajax_custom_list_nonce', true );
  12.  
  13. 		$wp_list_table = new My_List_Table();
  14. 		$wp_list_table->prepare_items();
  15.  
  16.         ob_start();
  17.         $wp_list_table->display();
  18.         $display = ob_get_clean();
  19.  
  20.         die(
  21.  
  22. 		    json_encode(array(
  23.  
  24. 		        "display" => $display,
  25.  
  26.             ))
  27.  
  28.         );
  29.  
  30. 	}
  31.  
  32. 	add_action('wp_ajax__ajax_ts_display', '_ajax_ts_display_callback');

Abbiamo impostato due actions ajax di WordPress:

  1. wp_ajax__ajax_ts_display richiama la funzione _ajax_ts_display_callback che cattura il metodo display della classe che abbiamo creato e restituisce un JSON con la tabella generata e il set di dati relativo alla prima pagina.
  2. wp_ajax__ajax_fetch_ts_history richiama la funzione _ajax_fetch_ts_history_callback che restituisce un JSON contenente le righe delle successive pagine attraverso il metodo ajax_response precedentemente impostato.

Impostiamo il javascript

E’ l’ora di impostare le chiamate Javascript. Per comodità utilizzeremo jQuery che è integrato nel backend di WordPress e permette di facilitarci un pò il lavoro.

Il codice jQuery che io posterò è una modifica del sorgente di questo validissimo articolo che è stato di forte ispirazione per il mio lavoro.

  1. 	function ajax_fetch_ts_script() {
  2.  
  3. 		$screen = get_current_screen();
  4.  
  5. 		if ( $screen->id != "NOME DELLA NOSTRA SCREEN" )
  6. 			return;
  7.  
  8. 		?>
  9.  
  10. 		<script type="text/javascript">
  11.  
  12. 			(function ($) {
  13.  
  14. 				list = {
  15.  
  16. 					display: function() {
  17.  
  18. 						$.ajax({
  19.  
  20. 							url: ajaxurl,
  21. 							dataType: 'json',
  22. 							data: {
  23. 								_ajax_custom_list_nonce: $('#_ajax_custom_list_nonce').val(),
  24. 								action: '_ajax_ts_display',
  25. 								filter: '<?php print $_REQUEST['filter']; ?>'
  26. 							},
  27. 							success: function (response) {
  28.  
  29. 								$("#ts-history-table").html(response.display);
  30.  
  31.                                 $("tbody").on("click", ".toggle-row", function(e) {
  32.                                     e.preventDefault();
  33.                                     $(this).closest("tr").toggleClass("is-expanded")
  34.                                 });
  35.  
  36. 								list.init();
  37. 							}
  38. 						});
  39.  
  40. 					},
  41.  
  42. 					init: function () {
  43.  
  44. 						var timer;
  45. 						var delay = 500;
  46.  
  47. 						$('.tablenav-pages a, .manage-column.sortable a, .manage-column.sorted a').on('click', function (e) {
  48. 							e.preventDefault();
  49. 							var query = this.search.substring(1);
  50.  
  51. 							var data = {
  52. 								paged: list.__query(query, 'paged') || '1',
  53. 								filter: '<?php echo $_REQUEST['filter']; ?>'
  54. 							};
  55. 							list.update(data);
  56. 						});
  57.  
  58.                         $('input[name=paged]').on('keyup', function (e) {
  59.  
  60. 							if (13 == e.which)
  61. 								e.preventDefault();
  62.  
  63. 							var data = {
  64. 								paged: parseInt($('input[name=paged]').val()) || '1',
  65. 								filter: '<?php echo $_REQUEST['filter']; ?>'
  66. 							};
  67.  
  68. 							window.clearTimeout(timer);
  69. 							timer = window.setTimeout(function () {
  70. 								list.update(data);
  71. 							}, delay);
  72. 						});
  73.  
  74. 						$('#email-sent-list').on('submit', function(e){
  75.  
  76. 							e.preventDefault();
  77.  
  78. 						});
  79.  
  80.                     },
  81.  
  82. 					/** AJAX call
  83. 					 *
  84. 					 * Send the call and replace table parts with updated version!
  85. 					 *
  86. 					 * @param    object    data The data to pass through AJAX
  87. 					 */
  88. 					update: function (data) {
  89.  
  90. 						$.ajax({
  91.  
  92. 							url: ajaxurl,
  93. 							data: $.extend(
  94. 								{
  95. 									_ajax_custom_list_nonce: $('#_ajax_custom_list_nonce').val(),
  96. 									action: '_ajax_fetch_ts_history',
  97. 								},
  98. 								data
  99. 							),
  100. 							success: function (response) {
  101.  
  102. 								var response = $.parseJSON(response);
  103.  
  104. 								if (response.rows.length)
  105. 									$('#the-list').html(response.rows);
  106. 								if (response.column_headers.length)
  107. 									$('thead tr, tfoot tr').html(response.column_headers);
  108. 								if (response.pagination.bottom.length)
  109. 									$('.tablenav.top .tablenav-pages').html($(response.pagination.top).html());
  110. 								if (response.pagination.top.length)
  111. 									$('.tablenav.bottom .tablenav-pages').html($(response.pagination.bottom).html());
  112.  
  113.                                 list.init();
  114. 							}
  115. 						});
  116. 					},
  117.  
  118. 					/**
  119. 					 * Filter the URL Query to extract variables
  120. 					 *
  121. 					 * @see http://css-tricks.com/snippets/javascript/get-url-variables/
  122. 					 *
  123. 					 * @param    string    query The URL query part containing the variables
  124. 					 * @param    string    variable Name of the variable we want to get
  125. 					 *
  126. 					 * @return   string|boolean The variable value if available, false else.
  127. 					 */
  128. 					__query: function (query, variable) {
  129.  
  130. 						var vars = query.split("&");
  131. 						for (var i = 0; i < vars.length; i++) {
  132. 							var pair = vars[i].split("=");
  133. 							if (pair[0] == variable)
  134. 								return pair[1];
  135. 						}
  136. 						return false;
  137. 					},
  138. 				}
  139.  
  140. 				list.display();
  141.  
  142. 			})(jQuery);
  143.  
  144. 		</script>
  145. 		<?php
  146.  
  147. 	}
  148.  
  149. 	add_action( 'admin_footer', 'ajax_fetch_ts_script' );

Abbiamo creato una action admin_footer che richiama la funzione ajax_fetch_ts_script .

Questa funzione controlla se la screen dalla quale è richiamata è uguale a quella che abbiamo impostato (ricordatevi di sostituire “NOME DELLA NOSTRA SCREEN” con il nome relativo alla screen della pagina in cui gira lo script).

In tal caso stanziamo un oggetto list con 3 metodi:

  1. display, che effettua la prima chiamata ajax e ci permette di mostrare il contenuto della tabella relativa alla prima pagina
  2. init, che gestisce i click sugli elementi di navigazione della tabella
  3. update, che tramite chiamata ajax permette di aggiornare la tabella in ogni pagina con i dati ottenuti

Conclusioni e precisazioni

Siamo felici finalmente! Abbiamo ottenuto un modo per caricare ogni pagina di risultati in modo asincrono su WordPress e possiamo ora arricchire il nostro javascript mostrando per esempio sequenze animate di caricamento tra l’inizio delle chiamate e il popolamento della tabella con i dati ottenuti.

Non so se sia la migliore soluzione, sono certo però che nel mio caso specifico funziona e ha risolto la mia problematica!

Se sei interessato al codice completo entra subito su Github.

Andrea Debernardi

Andrea Debernardi

Cofondatore di dueclic, sviluppatore e appassionato di tecnologia. Pur essendo fortemente orientato al web la mia curiosità continua e attrazione riguardo le novità non mi precludono nulla nella programmazione. Non mi piace avere limiti perciò tendo a colmarli continuamente.