WordPress Post from Front End using REST API and Vue.js Part II

The objective of this post is to demonstrate some Vue.js basics for working with the WordPress REST API. This post continues where the last post left off with the following additions.

  • List posts submitted by the current user
  • Select from the list to edit a post
  • Delete a post from the list
Source Code

Development

The original proof of concept details building the WordPress plugin, installing Vue.js, Webpack and configuring for development. Also included is information on creating the front end slug where the Vuejs app will run. The end product is a simple form to submit new posts from a custom WordPress page template. It is recommended that you read these prerequisites.

List Posts

App.vue

Above the form, replace the hardcoded heading text with a placeholder to render heading data that will be adding to the data model.

Below the form, add this markup to display the messages for loading and errors along with the list element for the posts. Note the vue.js v-if attributes to control visibility of the elements depending on the data model values. This is called conditional rendering.

<h3>{{ heading }}</h3>
<form v-on:submit.prevent="onSubmit">

...

</form>

<div v-if="loading">
    Loading...
</div>

<div v-if="error">
    {{ error }}
</div>

<div v-if="posts">
    <ul>
        <li v-for="post in posts">
            <a href="#" @click='editPost(post)'>{{ post.title.rendered }}</a>
        </li>
    </ul>
</div>
  • Note that an ellipsis … in the code snippets are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, examine the source code.

In the App.vue component script block, update the data model for the new elements.

...

data() {
  return {
    error: null,
    heading: 'Submit New Post',
    loading: null,
    post_id: null,
    post_title: null,
    post_content: null,
    posts: null
  }
},

...

Insert the getPosts () method to fetch all the posts for the current user and set the posts property of the data model. The endpoints for the WordPress REST API can be looked up at its base url.

...

methods: {
  getPosts () {
    this.error = this.posts = null
    this.loading = true
    var params = '?author='+ wp_api_vuejs_poc.current_user_id +'&status=any';

    $.ajax({
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts' + params,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
    }, this ))
    .fail( $.proxy( function( response ) {
      this.error = response;
    }, this ));
  },

...

}

Insert the mounted function to call the getPosts method when the app has rendered. If there are any posts to show when the page loads, they should be listed.

App.vue
...

methods: {
  mounted: function () {
    this.$nextTick(function () {
      this.getPosts ();
    })
  },

...

}

Development Build

In your CLI, build in development mode which enables watch on the files and incrementally recompiles as needed. This is faster than a prodcution build. Additionally, this unminified build is compatible with the Vue DevTools extension for Chrome for debugging and object inspection.

npm run dev

After the build is completed and watch is running, reload the page. Submit a new post to make sure it is working and gets added to the list.

Edit / Add Posts

In the markup that lists posts, the titles link contains a @click='editPost(post) attribute to call the editPost method passing it the post object. Insert the editPost method to accept the post object from the list and set the properties of the data model. Since this is a simple proof of concept, we are not comparing the post against the version on the server to make sure it is hasn’t been modified since the last fetch. Note that the heading property is being set as well to display the appropriate text.

...

methods: {
  editPost( post ) {
    this.heading = 'Edit Post'
    this.post_id = post.id
    this.post_title = post.title.rendered
    this.post_content = post.content.rendered
  },

...

}

When you save the changes to the App.vue file, the watch detects the changes and incrementally rebuilds the code. Refresh the browser, select a post from the list and verify that it is working as expected.

We need a way to Submit New Post without reloading the page. Add this markup to the bottom of the form under the submit input so we have a link that will call a newPost method.

...

    <span v-if="post_id">
        <a href="#" @click="newPost">New Post</a>
    </span>
</form>

Insert the newPost method to reset the post and heading properties to their default values.

...

methods: {
  newPost() {
    this.heading = 'Submit New Post'
    this.post_id = null
    this.post_title = null
    this.post_content = null
  },

...

}

In most cases, we should reset the form after fetching posts. Insert a call to newPosts inside the ajax callback function right after the posts object is populated with the response.

...

methods: {
  getPosts () {

  ...

    .done( $.proxy( function( response ) {
      this.loading = false;
      this.posts = response;
      this.newPost();
    }, this ))

...

}

Delete Posts

Insert this link markup in the list item element after the post title link.

...
    <a href="#" @click='delPost(post)' title="DELETE">[–]</a>
</li>

Lastly, insert the delPosts method.

...

methods: {
  delPost( post ) {
    $.ajax({
      method: "DELETE",
      url: wp_api_vuejs_poc.rest_url + 'wp/v2/posts/' + post.id,
      beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wp_api_vuejs_poc.nonce );
      }
    })
    .done( $.proxy( function() {
      this.getPosts();
    }, this ))
    .fail( $.proxy( function( response ) {
      console.log( response );
    }, this ));
  },

...

}

Resources

WordPress Post from Front End using REST API and Vue.js

This post is a simple proof of concept for using the new WordPress REST API to submit a new post draft from the front end. The form and user inputs are built using the Vue.js framework and vue-cli to create a simple Webpack build configuration.

Environment

This example was created using a fresh install of WordPress 4.7 served by XAMMP on Windows 10.
Nodejs version 6.9.1
NPM version 3.10.8
vue-cli version 2.6.0
git version 2.7.0

Plugin Development

Change to the plugins directory. For example,

cd c:/xampp/htdocs/wordpress/wp-content/plugins

Create the directory for the new plugin. For example,

mkdir wp-api-vuejs-poc

Entry Point

Create a plugin entry point php file in this new plugin directory. For example,

# change to the new directory
cd wp-api-vuejs-poc

# create a new file
touch wp-api-vuejs-poc.php

Add the following php code to the plugin entry point. At the very least, add the comment block at the top to register the plugin with WordPress.

wp-api-vuejs-poc.php
<?php
/**
 * Plugin Name: WP API Vue.js Proof of Concept
 * Plugin URL: http://example.com
 * Description: WordPress plugin to draft posts from the front end using the new REST API and Vue.js
 * Version: 1.0
 * Author:
 * Author URI: http://example.com
 * Text Domain: wp-api-vuejs-poc
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

if ( ! class_exists( 'WP_API_Vuejs_PoC' ) ) :

/**
 * Main WP_API_Vuejs_PoC Class.
 */
final class WP_API_Vuejs_PoC {

    /**
     * The single instance of the class.
     */
    protected static $_instance = null;

    /**
     * Ensures only one instance of WP_API_Vuejs_PoC is loaded or can be loaded.
     */
    public static function instance() {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function __construct() {
        $this->define_constants();
        $this->includes();
    }

    /**
     * Define Constants.
     */
    private function define_constants() {
        $this->define( 'WAVP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
        $this->define( 'WAVP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    }

    private function define( $name, $value ) {
        if ( ! defined( $name ) ) {
            define( $name, $value );
        }
    }

    /**
     * What type of request is this?
     *
     * @param  string $type admin, ajax, cron or frontend.
     * @return bool
     */
    private function is_request( $type ) {
        switch ( $type ) {
            case 'admin' :
                return is_admin();
            case 'ajax' :
                return defined( 'DOING_AJAX' );
            case 'cron' :
                return defined( 'DOING_CRON' );
            case 'frontend' :
                return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
        }
    }

    public function includes() {
        if ( $this->is_request( 'frontend' ) ) {
            include( 'class-api-vpoc-page.php' );
        }
    }

}

endif;

/**
 * Main instance of WP_API_Vuejs_PoC.
 * Returns the main instance of WAVP to prevent the need to use globals.
 */
function WAVP() {
    return WP_API_Vuejs_PoC::instance();
}

// Global for backwards compatibility.
$GLOBALS['wp-api-vuejs-poc'] = WAVP();

Class for Front End Page Request

In the class of the plugin entry point file above, the includes function contains a request type check. When the request is made from the front end, include the class file for the page. Let’s create that class file now, for example,

touch class-api-vpoc-page.php

Add the following php code to create the class. The constructor method is called on the newly-created class object where we are subscribing to events using hooks.

The action hook is called during the page processing event for script loading, at this point, call our page_scripts function to load the app javascript.

The filter hook is called during data processing, when the content is being loaded, call our page_content function to create the app bootstrap element.

class-api-vpoc-page.php
<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * WP_API_Vuejs_PoC_Page class.
 */
class WP_API_Vuejs_PoC_Page {

    /**
     * Access
     */
    private static $user_can = 'edit_posts';
    private static $page_slug = 'api-test';

    /**
     * Constructor.
     */
    public function __construct() {
        add_action( 'wp_enqueue_scripts', array( $this, 'page_scripts' ) );
        add_filter( 'the_content', array( $this, 'page_content' ) );
    }

    public function page_scripts() {
        if ( is_page( self::$page_slug ) ) {
            // load the Vue.js app
            wp_enqueue_script( 'wp-api-vuejs-poc', WAVP_PLUGIN_URL . 'dist/build.js', array(), false, true );

            // localize data for script
            wp_localize_script( 'wp-api-vuejs-poc', 'wp_api_vuejs_poc', array(
                'rest_url' => esc_url_raw( rest_url() ),
                'nonce' => wp_create_nonce( 'wp_rest' ),
                'success' => __( 'Post submitted', 'wp-api-vuejs-poc' ),
                'failure' => __( 'Post could not be processed.', 'wp-api-vuejs-poc' ),
                'current_user_id' => get_current_user_id()
                )
            );
        }
    }

    public function page_content($content) {
        if ( is_page( self::$page_slug ) ) {
            // output only to logged in users who can edit posts
            if ( is_user_logged_in() && current_user_can( self::$user_can ) ) {
                // app bootstrap element
                ob_start();?>
                <div id="app"></div>
                <?php
                $content .= ob_get_clean();
            }else{
                $content .=  sprintf( '<a href="%1s">%2s</a>', esc_url( wp_login_url() ), __( 'Log in', 'wp-api-vuejs-poc' ) );
            }
        }

        return $content;
    }

}

return new WP_API_Vuejs_PoC_Page();

The next page covers the Vue.js front end.