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.

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();
Categories
Web WordPress WordPress Plugins

WORDPRESS PLUGIN BOILERPLATE

What is Plugin Boiler Plate?
A foundation for WordPress Plugin Development that aims to provide a clear and consistent guide for building your plugins.

Every time I start working on a new plugin I find myself renaming files names, searching and replacing plugin-namePlugin_Name , the packages, sub packages names, etc. All these tasks take me around 5-10 minutes every time, and I don’t like repeat unnecessary tasks.

So I found an easy way to generate WordPress plugin basic structure with in seconds. Of course it saved a lot of time.

Please visit  WordPress Plugin Boilerplate to generate plugins boiler plate. Obviously it won’t generate all of the files you require for your plugin. You can add more files once you have your basic structure.

WordPress Plugin Boilerplate Generator Configuration
WordPress Plugin Boilerplate Generator Configuration

There is no standard way to write a plugin. People follow different ways to write plugins. Some plugins require OOP to be followed and some don’t. But this is the best OOP structure that I had ever found and used in my Plugins development.

Please visit WordPress Plugin Boilerplate GitHub repository to get the essence of the final plugin structure.

It is meant to be a starting point for plugin development, an object oriented way of creating a standardized plugin. Since it is coded with OOP principles, it is mainly intended for intermediate coders, but you can easily use it even as a beginner if you know what goes where. By the end of this article, you should know what’s what and how you can get started with it – regardless of your coding experience.
Lets dig deeper into it.

File Structure.
The boilerplate is meant to be used as a GitHub repository, so the main directory contains files commonly found in GitHub repos. The README.md file is a general readme and shows up on your main repository page as the description and details about the plugin. The .gitignore file is for setting files that git should ignore when working with files.
The main folder here plugin-name is where the plugin is stored. It should have at least one fine your-plugin-slug.php with all the plugin details (Plugin Name, Version, Description, Author, Plugin Slug).

<?php

/**
 * The plugin bootstrap file
 *
 * This file is read by WordPress to generate the plugin information in the plugin
 * admin area. This file also includes all of the dependencies used by the plugin,
 * registers the activation and deactivation functions, and defines a function
 * that starts the plugin.
 *
 * @link              www.authoruri.com
 * @since             1.0.0
 * @package           Your_Plugin_Slug
 *
 * @wordpress-plugin
 * Plugin Name:       Your Plugin Name
 * Plugin URI:        www.yourpluginurl.com
 * Description:       This is a short description of what the plugin does. It's displayed in the WordPress admin area.
 * Version:           1.0.0
 * Author:            Author Name
 * Author URI:        www.authoruri.com
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       your-plugin-slug
 * Domain Path:       /languages
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Currently plugin version.
 * Start at version 1.0.0 and use SemVer - https://semver.org
 * Rename this for your plugin and update it as you release new versions.
 */
define( 'YOUR_PLUGIN_SLUG_VERSION', '1.0.0' );

/**
 * The code that runs during plugin activation.
 * This action is documented in includes/class-your-plugin-slug-activator.php
 */
function activate_your_plugin_slug() {
	require_once plugin_dir_path( __FILE__ ) . 'includes/class-your-plugin-slug-activator.php';
	Your_Plugin_Slug_Activator::activate();
}

/**
 * The code that runs during plugin deactivation.
 * This action is documented in includes/class-your-plugin-slug-deactivator.php
 */
function deactivate_your_plugin_slug() {
	require_once plugin_dir_path( __FILE__ ) . 'includes/class-your-plugin-slug-deactivator.php';
	Your_Plugin_Slug_Deactivator::deactivate();
}

register_activation_hook( __FILE__, 'activate_your_plugin_slug' );
register_deactivation_hook( __FILE__, 'deactivate_your_plugin_slug' );

/**
 * The core plugin class that is used to define internationalization,
 * admin-specific hooks, and public-facing site hooks.
 */
require plugin_dir_path( __FILE__ ) . 'includes/class-your-plugin-slug.php';

/**
 * Begins execution of the plugin.
 *
 * Since everything within the plugin is registered via hooks,
 * then kicking off the plugin from this point in the file does
 * not affect the page life cycle.
 *
 * @since    1.0.0
 */
function run_your_plugin_slug() {

	$plugin = new Your_Plugin_Slug();
	$plugin->run();

}
run_your_plugin_slug();

Activation, Deactivation & Uninstall:
This file ( your-plugin-slug.php ) also includes activation and deactivation hooks. These hooks are fired when plugin is activated and deactivated. This file is a gateway to the functionality of the whole plugin. You can add your whole code in this file but its not recommended you should follow MVC separate your models, views and your controllers. Any code you want to run when the plugin is activated should go in includes/your-plugin-slug-activator.php. In this file, there is a class named Your_Plugin_Slug_Activator inside which there is a activate() method you should use.

<?php

/**
 * Fired during plugin activation
 *
 * @link       www.authoruri.com
 * @since      1.0.0
 *
 * @package    Your_Plugin_Slug
 * @subpackage Your_Plugin_Slug/includes
 */

/**
 * Fired during plugin activation.
 *
 * This class defines all code necessary to run during the plugin's activation.
 *
 * @since      1.0.0
 * @package    Your_Plugin_Slug
 * @subpackage Your_Plugin_Slug/includes
 * @author     Author Name <author@gmail.com>
 */
class Your_Plugin_Slug_Activator {

	/**
	 * Short Description. (use period)
	 *
	 * Long Description.
	 *
	 * @since    1.0.0
	 */
	public static function activate() {
		//activation code here
	}

}

The code you need to run on deactivation should be placed in includes/your-plugin-slug-deactivator.php. The deactivate() method within the Your_Plugin_Slug_Deactivator is what you’ll need to use.

<?php

/**
 * Fired during plugin deactivation
 *
 * @link       www.authoruri.com
 * @since      1.0.0
 *
 * @package    Your_Plugin_Slug
 * @subpackage Your_Plugin_Slug/includes
 */

/**
 * Fired during plugin deactivation.
 *
 * This class defines all code necessary to run during the plugin's deactivation.
 *
 * @since      1.0.0
 * @package    Your_Plugin_Slug
 * @subpackage Your_Plugin_Slug/includes
 * @author     Author Name <author@gmail.com>
 */
class Your_Plugin_Slug_Deactivator {

	/**
	 * Short Description. (use period)
	 *
	 * Long Description.
	 *
	 * @since    1.0.0
	 */
	public static function deactivate() {
		//deactivation code here
	}

}


Do you think this is a bit too complex? I don’t blame you! When you start using object oriented concepts you’ll see the benefit of this over procedural code. If nothing else, it provides a very obvious place to put your code which is in itself a huge help. For uninstallation, the recommended method is to use uninstall.php which is what WordPress Plugin Boilerplate does. Your code should be placed at the very bottom of that file. You should dump everything that your plugin has created it is a sign of good plugin development to clean what is of no use. WordPress will automatically call uninstall.php if found in the plugins main folder. You can also use uninstallation hook.

<?php

/**
 * Fired when the plugin is uninstalled.
 *
 * When populating this file, consider the following flow
 * of control:
 *
 * - This method should be static
 * - Check if the $_REQUEST content actually is the plugin name
 * - Run an admin referrer check to make sure it goes through authentication
 * - Verify the output of $_GET makes sense
 * - Repeat with other user roles. Best directly by using the links/query string parameters.
 * - Repeat things for multisite. Once for a single site in the network, once sitewide.
 *
 * This file may be updated more in future version of the Boilerplate; however, this is the
 * general skeleton and outline for how the file should work.
 *
 * For more information, see the following discussion:
 * https://github.com/tommcfarlin/WordPress-Plugin-Boilerplate/pull/123#issuecomment-28541913
 *
 * @link       www.authoruri.com
 * @since      1.0.0
 *
 * @package    Your_Plugin_Slug
 */

// If uninstall not called from WordPress, then exit.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
	exit;
}

//write uninstallation code here.

Adding Hooks

Hooks are handled by WordPress Plugin Boilerplate amazingly, but it may seem a bit unwieldy at first. All your hooks should be placed within includes/class-your-plugin-slug.php. More specifically, inside the Your_Plugin_Slug class, within two methods:

  • define_public_hooks() when adding a hook that is used on the front-end for public.
  • define_admin_hooks() when adding a hook that is used on the back-end for admins.

Instead of using add_action() or add_filter() as usual, you’ll need to do things slightly differently. Here is how you add an action.

$this->loader->add_action( 'init', $plugin_public, 'display_message' );

Here we have a loader class Class Your_Plugin_Slug_Loader that automatically adds actions and filters for us.
The first parameter is the name of the hook, the second is a reference to the public or admin object. For public hooks this should be $plugin_public, for admin hooks it should be $plugin_admin. The third parameter is the hooked callback function.

While it seems more convoluted it standardizes the addition of hooks completely, splitting them into two distinct groups in the process.

Public And Admin Content

WordPress Plugin Boilerplate splits hooks into admin/public groups but that’s not all. It splits all your code in the same way by asking you to write public-facing code in the public folder and admin-facing code in the admin folder.

Both folders contain cssjs and partials folders. You should place used CSS/JS assets into these folders and write templates and other reusable bits of HTML into the partials folder. It’s okay to create new files in the partials folder, in fact, that’s what it’s there for!

You should write your hooked functions in these folders as well, within the class in the respective directories. When we hooked the display_message function to inti hook above, we told WordPress Plugin Boilerplate where to look for it as well. Since we added this to the public facing side, WordPress Plugin Boilerplate expects it to be defined within the Your_Plugin_Slug_Public class which is in the public folder. Simply create this function within the class and write everything else as required.

Resources And Dependencies:
You can add other resources and dependencies in includes folder, admin or public folder depending on the use of that resource.

At first, using WordPress Plugin Boilerplate may seem like a hassle, but it will pay off in the end. You will come back a year later and know where everything is, your plugin development will be standardized across products and other developers will be able to figure out what’s going on as well.

Finally, don’t forget that a plugin providing a simple widget may not need such a framework. While the use of WordPress Plugin Boilerplate won’t slow down your plugin it does clog up the view if all you need is a few simple lines of code!

Cheers!

  • plugin-boiler-plate-overview
  • plugin-boiler-plate-admin-overview
  • plugin-boiler-plate-includes-overview
  • plugin-boiler-plate-public-overview

And if you have enjoyed this post, feel free to share it or subscribe to my newsletter below.

Categories
Web WordPress

WordPress Basic Terminologies 📃

This article will help beginners to get conformable with the terms used within the WordPress ecosystem.

Some of these terminologies might seem known to you depending on your level of experience with WordPress, and web development in general.

These terms will help you to better understand the WordPress official documentation, support articles, and other WordPress developers. Now, here are some terms which you will find handy when talking about WordPress .

  1. Back End – The back end refers to the area of your site where you can log in and manage everything from your Dashboard.
  2. Content Management System – A software which helps to manage content a website.
  3. cPanel – A web-based administration tool for managing your web hosting account.
  4. Database – A software used to store and manage data in an orderly fashion.( i.e. MSQL )
  5. Codex – The official WordPress manual with support articles, documentation, code snippets, and links to external resources.
  6. Default theme – The theme that comes by default with a fresh installation of WordPress. Also used as a fallback theme if the current theme breaks.
  7. DNS – Refers to the Domain Name System which maps domain names( like google.com) to its IP address.
  8. Domain name – A system used to assign easy to remember names to IP addresses of websites.
  9. DOM – An interface that allows programmers to dynamically access HTML and XML on a web page.
  10. Footer area – The horizontal area at the bottom of the website where widgets and copyright information are usually displayed.
  11. Header Image – A wide picture that can be set to appear at the top of your website.
  12. Front end – The user-facing part of your website where visitors can view and interact with your content.
  13. Gravatar – A service which lets users associate a global avatar (image or photo) with their email addresses.
  14. Hosting provider – A company which sells space on a web hosting server for a fee.
  15. IDE – An application which provides you with all the tools you need to develop and test software.
  16. Menu – A collection of links to pages, categories, and social media profiles on your website usually displayed in the navigation area.
  17. Meta – Can mean different things in WordPress depending on where it is used. Usually used to refer to administrative data.
  18. Navigation – A set of links on your website which helps site visitors navigate through your content.
  19. Nonce – A one-time token generated by WordPress to protect your site against unexpected or duplicate requests which can cause permanent or unintended consequences.
  20. Multisite – Create a network of WordPress sites from a single WordPress installation.
  21. Permalink – A permanent link to content on your website. Can be used to share unique pages on your website with others easily.
  22. Sidebar – A place on your website where you usually display the search bar, recent and popular posts, and other important widgets. A website can have more than one sidebar.
  23. XML-RPC – A remote procedure call (RPC) protocol which uses XML to encode its calls and HTTP as a transport mechanism.
  24. Toolbar – The small black bar just above your site from where you can access quick links to various parts of your website. Usually visible only to logged in users.
  25. Post – Also known as “articles” and sometimes incorrectly referred to as “blogs”. In WordPress, “posts” are articles that you write to populate your blog.
  26. Post Slug – A few lowercase words separated by dashes, describing a post and usually derived from the post title to create a user-friendly (that is, readable and without confusing characters) permalink. Post slug substitutes the “%posttitle%” placeholder in a custom permalink structure. Post slug should not be changed and is especially useful if the post title tends to be long or changes frequently.
  27. Post Type – Post type refers to the various structured data that is maintained in the WordPress posts table. Native (or built-in) registered post types are postpageattachmentrevision, and nav-menu-item. Custom post types are also supported in WordPress and can be defined with register_post_type(). Custom post types allow users to easily create and manage such things as portfolios, projects, video libraries, podcasts, quotes, chats, and whatever a user or developer can imagine.
    1. Related articles: Post Types
  28. Role – A role gives users permission to perform a group of tasks. When a user logs in and is authenticated, the user’s role determines which capabilities the user has, and each capability is permission to perform one or more types of task. All users with the same role normally have the same capabilities. For example, users who have the Author role usually have permission to edit their own posts, but not permission to edit other users’ posts. WordPress comes with six roles and over fifty capabilities in its role-based access system. Plugins can modify the system.
    1. Related article: Roles and Capabilities
    2. External link: Role-based access control (Wikipedia)
  29. Screen – In WordPress a screen is a web page used for managing part of a weblog (site) or network. The term ‘screen’ is used to avoid confusion with ‘page‘, which has a specific and different meaning in WordPress. For example, the web page used to manage posts is known as the Posts Screen.
    1. Related article: Class_Reference/WP_Screen
  30. Shortcode – A Shortcode is a technique for embedding a snippet of PHP code into the body of a page or other content item.

To learn more about WordPress terminology, have a look at the WordPress Glossary page.

WordPress Terms

And if you have enjoyed this post, feel free to share it or subscribe to my newsletter below.

Categories
Servers Web WordPress

How does the web work?

The Web is a subset of the Internet.

While the Internet makes up all communication between computers within any application, port, or protocol — the Web generally refers to networks of computers communicating over Hyper Text Transfer Protocol (HTTP), serving Hyper Text Markup Language (HTML) and related assets, such as images, stylesheets, and scripts.

All these technologies work together to create web sites and web applications, parsed by the web browsers and various Application Programming Interfaces (APIs).

When a user requests a web page in a web browser, the process typically looks like this:

  • Search, Social, or Direct Access: If a user types in a search phrase, and the browser bar also acts as a search bar, the web site of the default search engine is queried.
  • DNS Lookup: If the user types in an exact web address, or clicks a link from a social web site or search engine, a Domain Name Server lookup determines which computer should respond to the request.
    • Domain name servers can be overridden with a hosts file.
    • Some domain name servers can intercept, modify, and optimize content. For example, Cloudflare, which can add SSL, optimize HTML, optimize images, etc.
  • Connection & Headers: Browser negotiates either an insecure HTTP connection on default port 80, or a secure HTTPS connection on default port 443. Request headers and response headers are sent. These are viewable in the Web Inspector.
    • A web server doesn’t have to be running on default ports. For example, many development servers might load websites at localhost:8080 or any other arbitrary port.
  • Initial HTML & AJAX: If a browser request is the first request, HTML is most likely returned. This HTML usually contains references to hundreds of other files, which are requested and processed by the browser.
    • Initial HTML and AJAX requests, in our case, are typically handled by a Content Management System such as WordPress.
      • Each request spins up a new PHP process. One page might involve hundreds of HTTP requests!
        • Content Management Systems, such as WordPress, will typically not only spin up a programming language process, but also a database instance for content. This might be MySQLSQLiteMongoDB (NoSQL), or many others.
      • If WordPress is not in use, PHP might still be in use with another PHP-based CMS such as Laravel.
      • If PHP is not in use, the web server could be using any number of server-side technologies, such as static assets (plain HTML or JavaScript), a NodeJS framework such as AdonisJS, or another language such as Ruby, Java, Python, or C# (Microsoft ASP.NET).
  • Static Assets: If the requested file is a static asset, such as CSS, JavaScript, or images, a PHP process is typically not spun up (though PHP can serve these things!) Instead, they are typically served directly by the web server (ApacheNginx, a combination of these, or another server, such as Lighttpd or ASP.NET’s Kestrel server).
    • Serving static assets without PHP is faster than spinning up a PHP process — often by an order of magnitude.
    • Nginx is usually faster than Apache. For this reason, they are sometimes used together, with Nginx as a front-end proxy.
    • Static assets are sometimes offloaded to another server entirely — for example, a CDN such as Cloudflare CDNJetpack, or JSDelivr.
    • Each static asset, JavaScript, CSS, and images, require processing by the browser.
  • Once a web page is loaded, the user starts to interact with the page. From here, simple web pages might link to other pages, re-initiating the entire cycle. If JavaScript is in use, as with Decoupled WordPressReactJS, or plugins like Flying Pages, JavaScript might be used to update page content without reloading initial HTML.

Caching: It’s important to note — caching may take effect at almost every step of the connection process. The browser has a cache. DNS may have a cache (Cloudflare), the CMS may have a cache, and there may be various application-level caches (WordPress wp_object_cache, WordPress Transients API, or plugins such as W3 Total Cache) or server-level caches, such as RedisVarnish, or Nginx.

Resources

Whether you believe it or not, there are great resources out there that have helped hundreds of people with no technical background become web/WordPress developers. If you would like to explore this more for yourself, or just learn more about web development in general, here are some useful links:

And if you have enjoyed this post, feel free to share it or subscribe to my newsletter below.

Categories
React WordPress Plugins

React app embedded into WordPress page via short code

Goal: Create WordPress plugin which will inject React based web app\widget into normal WordPress page\post\sidebar using a shortcode.

Embedded react app useful when you need to create an complex fully interactive widget\app like a calculator for example, and you want it to be a part of WordPress page.

In this article I’ll describe one of the approaches to embedded react app into WordPress page. Although it may be not a perfect way, we’ll dig deeper into other build approaches in next articles.

Let’s split our big task into sub-tasks to better understand how we achieve it:

  1. Create WordPress plugin
    • it will enqueue our js\css and register shortcode to render root element of React app.
    • yes shortcodes are old school approach (compared to Gutenberg Blocks). But they work well for this use case and we keep it simpler for now.
    • it will also pass in App’s settings via data attribute of root element.
  2. Generate React app via npx create-react-app
    • the idea is to utilize a ready to use solution for scaffolding and Webpack configuration, so newbies like me .🙂 in react world could follow standard React tutorials to build his first React App.
    • also you can use approach explained in this article to simply inject your existing React App, generated via npx create-react-app into WP based site.
  3. Modify build\start npm scripts to work well with WordPress.
    • by default react build Webpack’s configuration will generate js files split in chunks – it makes it harder to link them via wp_enqueue_scripts as we used to in WordPress.
    • so we’ll write a code which will partially override config and Webpack will generate js\css which will be easy to use in WP.
  4. Write JSX\React code and build it.
  5. Insert shortcode [wp-react-app] and enjoy.

Result plugin can be fetched from github https://github.com/MuhammadFaizanHaidar/wp-react-app/

1. Create WordPress plugin

It will register a shortcode and enqueue public scripts and styles for our React App.

https://github.com/MuhammadFaizanHaidar/wp-react-app/blob/main/public/class-wp-react-app-public.php

NB: I use

define('ENV_DEV', true); // in wp-config.php

to load generated by yarn start css\js files while development, set it to false when you need to use css\js generated via yarn build.

In a shortcode I pass in json string of settings as data-default-settings attribute for our App’s root element.

https://github.com/MuhammadFaizanHaidar/wp-react-app/blob/main/includes/shortcodes/class-wp-react-app-short-codes.php

<?php

/**
 * Fired to add short codes
 *
 * @link       www.faizanhaidar.com
 * @since      1.0.0
 *
 * @package    Wp_React_App
 * @subpackage Wp_React_App/includes
 */

/**
 * Fired to add short codes.
 *
 * This class defines all code necessary functions to run during the short codes rendering.
 *
 * @since      1.0.0
 * @package    Wp_React_App
 * @subpackage Wp_React_App/includes
 * @author     Muhammad Faizan Haidar <me@faizanhaidar.com>
 */
class Wp_React_App_Short_Codes {
    /**
	 * The unique identifier of this plugin.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @var      string    $plugin_name    The string used to uniquely identify this plugin.
	 */
	protected $plugin_name;

	/**
	 * The current version of the plugin.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @var      string    $version    The current version of the plugin.
	 */
	protected $version;
    /**
     * Class constructor.
     */
    public function __construct( $version, $name ) {

        $this->version     = $version;
        $this->plugin_name = $name;
        add_shortcode(
            'wp-react-app',
            [ $this, 'wp_react_app_shortcode' ]
        );

    }

    /**
     * Shortcode which renders Root element for your React App.
     *
     * @return string
     */
    public function wp_react_app_shortcode() {

        /**
         * You can pass in here some data which if you need to have some settings\localization etc for your App,
         * so you'll be able for example generate initial state of your app for Redux, based on some settings provided by WordPress.
         */
        ob_start();
        $settings = array(
            'l18n'       => array(
                'main_title' => 'Hi this is your React app running in WordPress',
            ),
            'some_items' => array( 'lorem ipsum', 'dolor sit amet' ),
        );
        ?>
       /** 

        * data-default-settings passed as an attribute.

        */
        <div id="wp-react-app" data-default-settings="<?php esc_attr_e( wp_json_encode( $settings ) ); ?>">ecgosdhds</div>
        <?php
        $output = ob_get_contents();
        ob_end_clean();

        return $output;
    }
}

2. Generate React App

In your new plugin’s directory simply run npx create-react-app app and wait a bit.

We’ll use yarn add for the sake of brevity but you can use npm as well. Just pick one (anyway you can switch back any time if needed).

Let’s cleanup a bit via removing redundant files from src directory of app. These are:

  • logo.svg – because in WordPress we handle images URLs differently – you can use relative to plugin’s path links to images.
  • serviceWorker.js

As well as references to them in index.js and App.js

You can remove all files inside /app/public except index.html as we don’t need them.

Remove .git directory inside react app’s folder – as you’ll probably want to track changes via git but for a whole plugin and not just react part of it. And in .gitignore remove line

/build

So our main.js and main.css files which are result of build process will be tracked with git.

Next step is optional – we’ll install node-sass I just prefer to work with SCSS files rather then plain CSS, so in case you like it as well – just cd to your newly generated react app directory and then run yarn add node-sass as described here https://create-react-app.dev/docs/adding-a-sass-stylesheet/

Create scss folder under src and move css files into it, renaming them to .scss and change imports of these files in js to point to our new .scss files like:

import './scss/App.scss';

in App.js, and

import './scss/index.scss';

for index.js

3. Modify build\start npm scripts to work well with WordPress

In your React App’s directory, run yarn add rewire --dev – we use rewire to be able to redefine webpack.config provided by create-react-app boilerplate without a need to eject react app or modify config files which reside in node_modules

Now create new directory called scripts – here we’ll set our JS files which will do magic for us.

start-non-split.js – this file will change Webpack config on fly. So that it will not split JS code in chunks and will not inject hash into filename.

const externals = require( './externals' );
const rewire = require( 'rewire' );
const defaults = rewire( 'react-scripts/scripts/start.js' );
const configFactory = defaults.__get__( 'configFactory' );

defaults.__set__( 'configFactory', ( env ) => {
   const config = configFactory( env );

   config.externals = {
      ...config.externals,
      ...externals,
   };

   config.optimization.splitChunks = {
      cacheGroups: {
         default: false,
      },
   };
   config.optimization.runtimeChunk = false;
   config.output.filename = 'static/js/[name].js';
   config.output.chunkFilename = 'static/js/[name].js';
   return config;
} );

build-non-split.js – this is used for building production js\css files. It will again build them as single files making it easier to link in WP.

const externals = require( './externals' );
const rewire = require( 'rewire' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const defaults = rewire( 'react-scripts/scripts/build.js' );
const config = defaults.__get__( 'config' );

/* we inject our Externals to keep our bundles clean and slim */
config.externals = {
   ...config.externals,
   ...externals,
};

/*
 * we set jsonFunction of webpack to our custom one
 * so multiple js bundles built with webpack could be safely loaded,
 * as we are aware that our plugin isn't the only one which can be potentially built with webpack.
 */
config.output.jsonpFunction = 'YourReactApp_webpackJsonpjs';

/* we disable chunks optimization because we want single js\css file to be loaded by plugin. */
config.optimization.splitChunks = {
   cacheGroups: {
      default: false,
   },
};
config.optimization.runtimeChunk = false;
config.output.filename = 'static/js/[name].js';
config.output.chunkFilename = 'static/js/[name].js';

/*
* lets find `MiniCssExtractPlugin` type of object in plugins array and redefine it's options.
* And remove all unnecessary plugins.
*/
const disabledPlugins = [
   'GenerateSW',
   'ManifestPlugin',
   'InterpolateHtmlPlugin',
   'InlineChunkHtmlPlugin',
   'HtmlWebpackPlugin',
];
config.plugins = config.plugins.reduce( ( plugins, pluginItem ) => {

   if ( disabledPlugins.indexOf( pluginItem.constructor.name ) >= 0 ) {
      return plugins;
   }

   if ( pluginItem instanceof MiniCssExtractPlugin ) {
      plugins.push(
         new MiniCssExtractPlugin( {
            filename: 'static/css/[name].css',
            chunkFilename: 'static/css/[name].css',
         } )
      );
   } else {
      plugins.push( pluginItem );
   }

   return plugins;
}, [] );

externals.js – this file contains shared scripts which can be provided by WP Core. So we don’t need them to be bundled into our build.

module.exports = {
   react: 'React',
   'react-dom': 'ReactDOM',
};

In my case after I’ve marked React and ReactDOM as external ones JS bundle’s size decreased from 160kb to 56kb – those get’s loaded from wp-includes as a dependency any way, but imagine you have bunch of such Apps and each will load extra version of bundled React library. To overcome this we mark them as external – this way we keep our bundles clean and light, and at the same time we leverage WordPress scripts dependency manager to link necessary dependencies for us.

Then open package.json file and in scripts section update start command to

"start": "node ./scripts/start-non-split.js",

and build command to

"build": "node ./scripts/build-non-split.js"

So your package.json file will look like this:

{
  "name": "app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "start": "node ./scripts/start-non-split.js",
    "build": "node ./scripts/build-non-split.js",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "node-sass": "^4.14.0",
    "rewire": "^5.0.0"
  }
}

4. Write JSX\React code and build it.

Now we are almost ready, let’s update index.js file which initializes our React app on root element inserted via shortcode.

import React from "react";
import ReactDOM from "react-dom";
import "./scss/index.scss";
import App from "./App";

const rootEl = document.getElementById("wp-react-app");
if (rootEl) {
  const settings = JSON.parse( rootEl.getAttribute( 'data-default-settings' ) );
  ReactDOM.render(
    <React.StrictMode>
      <App settings={settings} />
    </React.StrictMode>,
    rootEl
  );
}

Here we also read initialization settings data provided by WP, and pass them into App as prop.

If you’ll use Redux you can use these data to generate initial state for App.

Let’s update our App.js file to utilize our settings from props.

import React from 'react';
import './scss/App.scss';

function App(props) {

  const items = props.settings.some_items || [];

  return (
    <div className="App">
      <header className="App-header">
        <h1>{ props.settings.l18n.main_title }</h1>
        <ul>
          {
            items.map((item, index)=>{
              return (
                  <li key={index}>{item}</li>
              )
            })
          }
        </ul>
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Now we are ready to test everything together.

  • activate our plugin and place [wp-react-app] shortcode on page.
  • in terminal cd to plugin’s directory /app and run yarn build – it will build js\css then just open a page where you’ve placed a shortcode and app will be rendered there.

For development:

  • add define('ENV_DEV', true); to your wp-config.php
  • in plugin’s /app sub directory run yarn start it will build css\js and spin up a dev server to serve updated css\js whenever you make change in source files. (yarn doesn’t support live reloading yet but for quickstart it’s enough).
  • Yarn will also open a new tab in browser leading to localhost:3000 and showing error message that React is not defined just close and ignore it. It says React is not defined just because we’ve declared it as external. And we haven’t bundled it into our result main.js
  • It will work inside WordPress page because we’ve declared wp-element as dependency in our plugin, so WordPress will load React on it’s own.
  • So just refresh your WordPress page where you’ve placed our new shortcode and here’s your React App inside WordPress page.
  • NB: in ENV_DEV true mode you need to yarn start otherwise it will not be able to load css\js, set it to false or comment out for production.

Happy codding 🙂

Final view after short code insertion. Hurray ! You have successfully embedded react app into WordPress page.

react-app-embedded-into-wordpress-page

And if you have enjoyed this post, feel free to share it or subscribe to my newsletter below.

%d bloggers like this: