Categories
Metaboxes Web WordPress WordPress Plugins

WP List Table inside metabox tips & tricks

This blog will help WordPress developers to overcome the hurdles. That they will face while using WP List Table inside a metabox within a post/cpt.

WPCS & WP List Table

Being a big follower of WPCS and WP List Table class being a core functionality. And easy to extend I always prefer to use it for custom listings so I don’t have to take care of the core standards.

Open source contribution

First I tried it 2 years ago in my WordPress open source plugin Attendance Management For LifterLMS .( To see which approach I do use to develop a plugin please click here ). Please see the attachment below.

attendances-list-table

But at that time I was not using any bulk actions so I used it just for displaying attendances count. But I had an issue while saving/updating that post. As it kept redirecting to the edit.php each time I save or update the post. After surfing the google and trouble shooting. I solved the problem by overriding the following function in the derived class.

    /**
	 * Generate the table navigation above or below the table.
	 *
	 * @since 1.0.0
	 * @access protected
	 *
	 * @param string $which
	 */
	protected function display_tablenav( $which ) {

		// REMOVED NONCE -- INTERFERING WITH SAVING POSTS ON METABOXES
		// Add better detection if this class is used on meta box or not.
		/*
		if ( 'top' == $which ) {
			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
		}
		*/

		?>
		<div class="tablenav <?php echo esc_attr( $which ); ?>">
	
			<div class="alignleft actions bulkactions">
				<?php $this->bulk_actions( 'bottom' ); ?>
			</div>
			<?php
			$this->extra_tablenav( $which );
			$this->pagination( $which );
			?>
	
			<br class="clear"/>
		</div>
		<?php
	}

Debugging WP List Table

Above function was generating an extra nonce. Which was manipulating post’s default nonce so it never reached to the save process. By overriding this function and commenting out the nonce part it worked for me as now save/update was working fine. But I was still having issue with my list table search method. When I clicked on the search button WordPress gets it as a call to save the post. So it takes to the save post method. And I lost my search parameters after redirection to the same post. So after troubleshooting I found a trick and that was the save post hook.

The issue was, when clicking the search submit button it redirected to the same post with a post_updated message. What happens here is basically post_update hook fires. Which redirects to the same post and I can’t get the search query_args. What I did is, I captured the update and post save hook and added query_args. Then redirected to the respective post and hence it worked. WP List table search box input has name “s” so we have to look for “s”( $_GET[“s”], $_POST[“s”] ) Find the code below please:

 add_action( 'save_post','save_query_string', 100, 3 );
 add_action( 'post_updated','save_query_string', 10, 3 );
 function save_query_string( $post_id, $post, $update ) {
        $post_type = get_post_type($post);
        $search_term  = isset( $_POST['s'] ) ? trim( $_POST['s'] ) : "";
        if ( $search_term == "" ) {
            $search_term  = isset( $_GET['s'] ) ? trim( $_GET['s'] ) : "";
        }
        if ( $post_type == 'product' && $search_term != "" ) {
            wp_safe_redirect( add_query_arg( 's', $search_term, $_POST['_wp_http_referer'] ) );
            exit;
        }
    }

Ask questions on exchanges

I had asked the same question 2 years back on WordPress stack exchange also.

So that worked back then. But recently I was developing a plugin for my client. And I wanted to display some details to a custom post type. Again I encountered the same problem but this time what I was using extra was bulk action functionality. I was only having this issue when I was using the bulk actions. So I knew that the issue is with bulk actions.

So I started debugging bulk_actions( $which = ” ) function. And finally found that the issue was with the following line of code.

echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";

The select filed has a name attribute which after calculation is equal to action or action2 and on save post this $_POST[‘action’] == ”some-action” but WordPress save post is not expected to receive this value. So that’s why it redirects to the edit.php. I resolved this just by changing the name attribute of the select to any desired value. Like in my case I changed it to actions i.e. name=”actions”.

I also had asked a questions related to this issue on WordPress stack exchange.

Now lets discuss the ways to handle bulk actions. In same post update/save hook you will find actions and other required data in $_POST either you can redirect to same edit page by adding query parameters and look for these parameters in your list table class and process bulk actions or you can just process bulk actions is same save/update hook and see updated results.

WP List Table Code compatible with MetaBoxes

Please find the whole working code below. You can use in your metabox on any post type by changing it according to your needs.

<?php
/**
 * Generates The User Grade Listing for Admin
 */
if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}


class Class_Conditional_Shortcode_Questions_Listing extends WP_List_Table {
	// define dataset for WP_List_Table => data

	/** Class constructor */
	public function __construct() {

		parent::__construct(
			array(
				'singular' => esc_html__( 'Question', 'conditional-shortcode' ), // singular name of the listed records
				'plural'   => esc_html__( 'Questions', 'conditional-shortcode' ), // plural name of the listed records
				'ajax'     => true, // does this table support ajax?
			)
		);
	}


	/**
	 * Function to filter data based on order , order_by & searched items
	 *
	 * @since 1.0.0
	 * @param string $orderby
	 * @param string $order
	 * @param string $search_term
	 * @return array $users_array()
	 */
	public function list_table_data_fun( $orderby = '', $order = '', $search_term = '' ) {

		$args            = array();
		$questions_array = array();
		$questions       = '';
		$flag            = false;
		if ( ! empty( $search_term ) ) {

			$args = array(
				'fields'         => 'ids',
				'search'         => intval( $search_term ),
				'post_type'      => 'assessment_question',
				'posts_per_page' => 1,
			);
			$flag = false;
		} else {
			if ( $order == 'asc' && $orderby == 'id' ) {
				$args = array(

					'orderby'        => 'ID',
					'order'          => 'ASC',
					'fields'         => 'ids',
					'post_type'      => 'assessment_question',
					'posts_per_page' => -1,
				);
			} elseif ( $order == 'desc' && $orderby == 'id' ) {
					$args = array(
						'orderby'        => 'ID',
						'order'          => 'DESC',
						'fields'         => 'ids',
						'post_type'      => 'assessment_question',
						'posts_per_page' => -1,
					);

			} elseif ( $order == 'desc' && $orderby == 'title' ) {
					$args = array(
						'orderby'        => 'name',
						'order'          => 'DESC',
						'fields'         => 'ids',
						'post_type'      => 'assessment_question',
						'posts_per_page' => -1,
					);
			} elseif ( $order == 'asc' && $orderby == 'title' ) {
				$args = array(
					'orderby'        => 'name',
					'order'          => 'ASC',
					'fields'         => 'ids',
					'post_type'      => 'assessment_question',
					'posts_per_page' => -1,
				);
			} else {
				$args = array(
					'orderby'        => 'ID',
					'order'          => 'DESC',
					'fields'         => 'ids',
					'post_type'      => 'assessment_question',
					'posts_per_page' => -1,
				);
				$flag = true;
			}
		}
		$questions = get_transient( 'pd_questions' );

		if ( $flag == false ) {
			$question = get_post( $search_term );
			$questions   = array();
			if ( $question && get_post_type( $question ) == 'assessment_question' ) {
				$questions   = array();
				$questions[] = $search_term;
			}
		} elseif ( $flag == true && ! $questions ) {
			$questions = get_posts( $args );
			set_transient( 'pd_questions', $questions, 1 * DAY_IN_SECONDS );
		}
		if ( count( $questions ) > 0 ) {
			foreach ( $questions as $question_id ) {
				$question          = get_post_meta( $question_id ?? 0, CONDITIONAL_SHORTCODE_ASSESSMENT_QUESTION_META, true )['question'] ?? 'NA';
				$questions_array[] = array(
					'id'       => $question_id,
					'title'    => '<b>' . get_the_title( $question_id ) . '</b>',
					'question' => $question,
				);

			}
		}

		return $questions_array;
	}
	/**
	 * Prepares items to display.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function prepare_items() {

		$orderby = sanitize_text_field( isset( $_GET['orderby'] ) ? trim( $_GET['orderby'] ) : '' );
		$order   = sanitize_text_field( isset( $_GET['order'] ) ? trim( $_GET['order'] ) : '' );

		$search_term = sanitize_text_field( isset( $_POST['s'] ) ? trim( $_POST['s'] ) : '' );
		if ( $search_term == '' ) {

			$search_term = sanitize_text_field( isset( $_GET['s'] ) ? trim( $_GET['s'] ) : '' );
		}

		$datas = $this->list_table_data_fun( $orderby, $order, $search_term );

		$per_page     = 30;
		$current_page = $this->get_pagenum();
		$total_items  = count( $datas );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
			)
		);

		$this->items = array_slice( $datas, ( ( $current_page - 1 ) * $per_page ), $per_page );

		$columns  = $this->get_columns();
		$hidden   = $this->get_hidden_columns();
		$sortable = $this->get_sortable_columns();

		$this->_column_headers = array( $columns, $hidden, $sortable );
		$this->process_bulk_action();
		$this->process_action();
	}

	/**
	 * Returns bulk actions.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public function get_bulk_actions() {

		return array(
			'add_questions'    => esc_html__( 'Add Questions', 'conditional-shortcode' ),
			'remove_questions' => esc_html__( 'Remove Questions', 'conditional-shortcode' ),
		);

	}
	/**
	 * Displays the bulk actions dropdown.
	 *
	 * @since 1.0.0
	 *
	 * @param string $which The location of the bulk actions: 'top' or 'bottom'.
	 *                      This is designated as optional for backward compatibility.
	 */
	protected function bulk_actions( $which = '' ) {
		if ( is_null( $this->_actions ) ) {
			$this->_actions = $this->get_bulk_actions();

			/**
			 * Filters the items in the bulk actions menu of the list table.
			 *
			 * The dynamic portion of the hook name, `$this->screen->id`, refers
			 * to the ID of the current screen.
			 *
			 * @since 3.1.0
			 * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
			 *
			 * @param array $actions An array of the available bulk actions.
			 */
			$this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores

			$two = '';
		} else {
			$two = '2';
		}

		if ( empty( $this->_actions ) ) {
			return;
		}

		echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . esc_html__( 'Select bulk action', 'conditional-shortcode' ) . '</label>';
		echo '<select name="actions' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
		echo '<option value="-1">' . esc_html__( 'Bulk actions', 'conditional-shortcode' ) . "</option>\n";

		foreach ( $this->_actions as $key => $value ) {
			if ( is_array( $value ) ) {
				echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n";

				foreach ( $value as $name => $title ) {
					$class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : '';

					echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n";
				}
				echo "\t" . "</optgroup>\n";
			} else {
				$class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : '';

				echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n";
			}
		}

		echo "</select>\n";

		submit_button( esc_html__( 'Apply', 'conditional-shortcode' ), 'action', '', false, array( 'id' => "doaction$two" ) );
		echo "\n";
	}

	/**
	 * Get all columns.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public function get_columns() {

		$columns = array(
			'cb'       => '<input type="checkbox" />',
			'id'       => esc_html__( 'ID', 'conditional-shortcode' ),
			'title'    => esc_html__( 'Title', 'conditional-shortcode' ),
			'question' => esc_html__( 'Question', 'conditional-shortcode' ),
			'action'   => esc_html__( 'Action', 'conditional-shortcode' ),
		);

		return $columns;
	}

	/**
	 * Returns hidden columns.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public function get_hidden_columns() {
		return array( '' );
	}

	/**
	 * outputs sortable columns.
	 *
	 * @since 1.0.0
	 * @return array
	 */
	public function get_sortable_columns() {
		return array(
			'title' => array( 'title', true ),
			'id'    => array( 'id', true ),
		);

	}

	/**
	 * Generate the table navigation above or below the table.
	 *
	 * @since 1.0.0
	 * @access protected
	 *
	 * @param string $which
	 */
	protected function display_tablenav( $which ) {

		// REMOVED NONCE -- INTERFERING WITH SAVING POSTS ON METABOXES
		// Add better detection if this class is used on meta box or not.
		/*
		if ( 'top' == $which ) {
			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
		}
		*/

		?>
		<div class="tablenav <?php echo esc_attr( $which ); ?>">
	
			<div class="alignleft actions bulkactions">
				<?php $this->bulk_actions( 'bottom' ); ?>
			</div>
			<?php
			$this->extra_tablenav( $which );
			$this->pagination( $which );
			?>
	
			<br class="clear"/>
		</div>
		<?php
	}

	/**
	 * Returns default columns.
	 *
	 * @since 1.0.0
	 * @param array  $item
	 * @param string $column_name
	 * @return string
	 */
	public function column_default( $item, $column_name ) {
		$post_id = get_the_ID();
		switch ( $column_name ) {
			case 'cb':
			case 'id':
			case 'title':
			case 'question':
				return $item[ $column_name ];
			case 'action':
				$questionnaires = get_post_meta( $item['id'], 'cs_selected_ques_id', true );
				if ( $questionnaires == '' ) {
					$questionnaires = array();
				}
				if ( ! in_array( $post_id, $questionnaires ) ) {
					return '<a href="?post=' . $post_id . '&action=edit&actions=add_question&question_id=' . $item['id'] . '&questionnaire_id=' . $post_id . '">Add Question</a>';
				} else {
					return '<a href="?post=' . $post_id . '&action=edit&actions=remove_question&question_id=' . $item['id'] . '&questionnaire_id=' . $post_id . '">Remove Question</a>';
				}
			default:
				return 'no value';

		}

	}

	/**
	 * Displays each column title.
	 *
	 * @since 1.0.0
	 * @param string $item
	 * @return string
	 */
	public function column_title( $item ) {
		$post_id        = get_the_ID();
		$questionnaires = get_post_meta( $item['id'], 'cs_selected_ques_id', true );
		if ( $questionnaires == '' ) {
			$questionnaires = array();
		}
		if ( ! in_array( $post_id, $questionnaires ) ) {
			$action = array(
				'edit' => sprintf( '<a href="?post=%d&action=%s&actions=%s&question_id=%d&questionnaire_id=%d">Add Question</a>', $post_id, 'edit', 'add_question', $item['id'], $post_id ),
			);
		} else {
			$action = array(
				'edit' => sprintf( '<a href="?post=%d&action=%s&actions=%s&question_id=%d&questionnaire_id=%d">Remove Question</a>', $post_id, 'edit', 'remove_question', $item['id'], $post_id ),
			);
		}
		return sprintf( '%1$s %2$s', $item['title'], $this->row_actions( $action ) );
	}

	/**
	 * Column for checkboxes.
	 *
	 * @since 1.0.0
	 * @param array $item
	 * @return string
	 */
	function column_cb( $item ) {
		return sprintf(
			'<input type="checkbox" name="add-questions[]" value="%d" />',
			$item['id']
		);
	}

	/**
	 * Default title for no items.
	 *
	 * @return void
	 */
	function no_items() {
		esc_html_e( 'No Questions Found.', 'conditional-shortcode' );
	}

	/**
	 * Gets the current action selected from the bulk actions dropdown.
	 *
	 * @since 1.0.0
	 *
	 * @return string|false The action name. False if no action was selected.
	 */
	public function current_action() {
		if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) {
			return false;
		}

		if ( isset( $_REQUEST['actions'] ) && -1 != $_REQUEST['actions'] ) {
			return $_REQUEST['actions'];
		}

		return false;
	}

	/**
	 * Processes single actions.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function process_action() {

		// security check!
		if ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['_wpnonce'] ) ) {

			$nonce  = filter_input( INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING );
			$action = 'bulk-' . $this->_args['plural'];

			if ( ! wp_verify_nonce( $nonce, $action ) ) {
				wp_die( esc_html_e( 'Nope! Security check failed!', 'conditional-shortcode' ) );
			}
		}

		$action        = $this->current_action();
		$question      = isset( $_GET['question_id'] ) ? $_GET['question_id'] : '';
		$questionnaire = get_the_ID();
		if ( $question && $questionnaire ) {
			switch ( $action ) {

				case 'add_question':
					$questionnaires = get_post_meta( $question, 'cs_selected_ques_id', true );
					if ( $questionnaires == '' ) {
						$questionnaires = array();
					}
					if ( ! in_array( $questionnaire, $questionnaires ) ) {
						$questionnaires[] = $questionnaire;
						update_post_meta( intval( $question ), 'cs_selected_ques_id', $questionnaires );
						delete_transient( 'pd_questions_' . $questionnaire );
					}

					break;

				case 'remove_question':
					$questionnaires = get_post_meta( $question, 'cs_selected_ques_id', true );
					if ( $questionnaires == '' ) {
						$questionnaires = array();
					}
					if ( ( $key = array_search( $questionnaire, $questionnaires ) ) !== false ) {
						unset( $questionnaires[ $key ] );
						delete_transient( 'pd_questions_' . $questionnaire );
						update_post_meta( intval( $question ), 'cs_selected_ques_id', $questionnaires );
					}

					break;

				default:
					// do nothing or something else
					return;
				break;
			}
		}
		return;
	}

	/**
	 * Processes bulk actions.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function process_bulk_action() {

		// security check!
		if ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['_wpnonce'] ) ) {

			$nonce  = filter_input( INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING );
			$action = 'bulk-' . $this->_args['plural'];

			if ( ! wp_verify_nonce( $nonce, $action ) ) {
				wp_die( esc_html_e( 'Nope! Security check failed!', 'conditional-shortcode' ) );
			}
		}

		$action        = $this->current_action();
		$questions     = isset( $_GET['questions'] ) ? $_GET['questions'] : '';
		$questionnaire = get_the_ID();
		if ( $questions && $questionnaire ) {
			switch ( $action ) {

				case 'add_questions':
					foreach ( $questions as $question ) {
						$questionnaires = get_post_meta( $question, 'cs_selected_ques_id', true );
						if ( $questionnaires == '' ) {
							$questionnaires = array();
						}
						if ( ! in_array( $questionnaire, $questionnaires ) ) {
							$questionnaires[] = $questionnaire;
							update_post_meta( intval( $question ), 'cs_selected_ques_id', $questionnaires );
							delete_transient( 'pd_questions_' . $questionnaire );
						}
					}
					break;

				case 'remove_questions':
					foreach ( $questions as $question ) {
						$questionnaires = get_post_meta( $question, 'cs_selected_ques_id', true );
						if ( $questionnaires == '' ) {
							$questionnaires = array();
						}
						if ( ( $key = array_search( $questionnaire, $questionnaires ) ) !== false ) {
							unset( $questionnaires[ $key ] );
							delete_transient( 'pd_questions_' . $questionnaire );
							update_post_meta( intval( $question ), 'cs_selected_ques_id', $questionnaires );
						}
					}
					break;

				default:
					// do nothing or something else
					return;
				break;
			}
		}
		return;
	}


}


/**
 * Shows the List table for all questions.
 *
 * @since 1.0.0
 * @return void
 */
function conditional_shortcode_questions_list_table_layout() {
	$table = new Class_Conditional_Shortcode_Questions_Listing();

	printf( '<div class="wrap" id="wpse-list-table"><h2>%s</h2>', __( '', 'conditional-shortcode' ) );

	//echo '<form id="wpse-list-table-form" method="post">';

	$page  = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRIPPED );
	$paged = filter_input( INPUT_GET, 'paged', FILTER_SANITIZE_NUMBER_INT );

	printf( '<input type="hidden" name="page" value="%s" />', $page );
	printf( '<input type="hidden" name="paged" value="%d" />', $paged );

	$table->prepare_items(); // this will prepare the items AND process the bulk actions
	$table->search_box( esc_html__( 'Search question by id', 'conditional-shortcode' ), 'conditional-shortcode' ); // Needs To be called after $myRequestTable->prepare_items()
	$table->display();

	//echo '</form>';

	echo '</div>';

}

conditional_shortcode_questions_list_table_layout();