WP Page Builder From Scratch #2: Features, Design, and UI

This is Part #2 of Custom Page Builder Tutorials Series. Read the intro here.

In this post I will explain how to create page builder from this:

page-builder-setting-selected

to this:

page-builder-template-unstyled

to this fancy drag-and-drop UI:

To follow this tutorial easier, you can download the source code for this tutorial:

Deciding Page Builder Features

Page builder is a way to manage content using drag-and-drop interface, where content added as rows, and we can add new rows, delete rows, etc. Each rows can have different options/settings.

In this tutorial, I decided to make it as simple as possible and only have 2 type of rows:

  1. Rows with 1 column content, and
  2. Rows with 2 columns content.

In rows with 1 column content, only one text area. In rows with 2 columns content, we will have two text areas.

To add this “rows” of content. We need an action buttons, one for each type of rows.

In earlier post ( Part #1: Create Page Builder Page Template ), we create a placeholder for the Page Builder settings in Page Edit Screen using edit_form_after_editor hook.

We need to edit that function and create 3 main element:

/* Add page builder form after editor */
add_action( 'edit_form_after_editor', 'fx_pbbase_form_callback', 10, 2 );

/**
 * Page Builder Form Callback
 * @since 1.0.0
 */
function fx_pbbase_form_callback( $post ){
    if( 'page' !== $post->post_type ){
        return;
    }
?>
    <div id="fx-page-builder">

        <?php /* This is where we gonna add & manage rows */ ?>
        <div class="fxpb-rows">
            <p class="fxpb-rows-message">This is where we manage rows.</p>
        </div><!-- .fxpb-rows -->

        <?php /* This is where our action buttons to add rows */ ?>
        <div class="fxpb-actions">
        </div><!-- .fxpb-actions -->

        <?php /* Rows template (Going to be hidden) */ ?>
        <div class="fxpb-templates">
        </div><!-- .fxpb-templates -->

    </div><!-- .fx-page-builder -->
<?php
}

The 3 elements are:

Rows Area ( .fxpb-rows )

this is where we manage rows, drag and drop. Where the magic happens.

As default  this area is empty. We are going to leave it as is. Just add a paragraph/empty row message as placeholder.

Action Buttons Area ( .fxpb-actions )

This is where we add buttons to add new rows:

<div class="fxpb-actions">
    <a href="#" class="fxpb-add-row button-primary button-large" data-template="col-1">Add 1 Column</a>
    <a href="#" class="fxpb-add-row button-primary button-large" data-template="col-2">Add 2 Columns</a>
</div><!-- .fxpb-actions -->

As you see, in the button, we have data-template attribute, we are going to use this in the JS script to clone the template.

Row Templates Area ( .fxpb-templates )

This is where we add “rows templates” so we can clone it when we click “Add rows button”. This element will be hidden later.

In this templates we add two “row templates”:

1 Column Row Template

<div class="fxpb-templates">

    <?php /* == This is the 1 column row template == */ ?>
    <div class="fxpb-row fxpb-col-1">

        <div class="fxpb-row-title">
            <span class="fxpb-handle dashicons dashicons-sort"></span>
            <span class="fxpb-row-title-text">1 Column</span>
            <span class="fxpb-remove dashicons dashicons-trash"></span>
        </div><!-- .fxpb-row-title -->

        <div class="fxpb-row-fields">
            <textarea class="fxpb-row-input" name="" data-field="content" placeholder="Add HTML here..."></textarea>
            <input class="fxpb-row-input" type="hidden" name="" data-field="type" value="col-1">
        </div><!-- .fxpb-row-fields -->

    </div><!-- .fxpb-row.fxpb-col-1 -->

2 Columns Row Template

    <?php /* == This is the 2 columns row template == */ ?>
    <div class="fxpb-row fxpb-col-2">

        <div class="fxpb-row-title">
            <span class="fxpb-handle dashicons dashicons-sort"></span>
            <span class="fxpb-row-title-text">2 Columns</span>
            <span class="fxpb-remove dashicons dashicons-trash"></span>
        </div><!-- .fxpb-row-title -->

        <div class="fxpb-row-fields">
            <div class="fxpb-col-2-left">
                <textarea class="fxpb-row-input" name="" data-field="content-1" placeholder="1st column content here..."></textarea>
            </div><!-- .fxpb-col-2-left -->
            <div class="fxpb-col-2-right">
                <textarea class="fxpb-row-input" name="" data-field="content-2" placeholder="2nd column content here..."></textarea>
            </div><!-- .fxpb-col-2-right -->
            <input class="fxpb-row-input" type="hidden" name="" data-field="type" value="col-2">
        </div><!-- .fxpb-row-fields -->

    </div><!-- .fxpb-row.fxpb-col-2 -->

</div><!-- .fxpb-templates -->

As you see, it’s a simple template. This templates are basically just empty inputs with all needed HTML structure.

In each template we also have hidden input with data-field attribute with value type, this hidden input row type identifier (1 column / 2 columns).

Each fields (in this case text areas and hidden inputs) all have CSS class .fxpb-row-input, this is for easier targeting using JS. The name attribute all empty, because we are going to add that dynamically using JS too.

Note:
For more complex system, I recommend using underscore.js template system.

Each “templates” have “title” area with two icon, one as handle for drag-and-drop operation (re-order), and another one is trash icon to delete the row.

This is the screenshot of the non styled HTML:

page-builder-template-unstyled

Now the next step is to design the UI.

Designing Page Builder UI

First we need to enqueue/load CSS and JS in Page Edit Screen. Simply add our script in our “admin_enqueue_scripts” function from last tutorial.

/* Admin Script */
add_action( 'admin_enqueue_scripts', 'fx_pbbase_admin_scripts' );

/**
 * Admin Scripts
 * @since 1.0.0
 */
function fx_pbbase_admin_scripts( $hook_suffix ){
    global $post_type;

    /* In Page Edit Screen */
    if( 'page' == $post_type && in_array( $hook_suffix, array( 'post.php', 'post-new.php' ) ) ){

        /* Load Editor Toggle Script */
        wp_enqueue_script( 'fx-pbbase-editor-toggle', FX_PBBASE_URI . 'assets/editor-toggle.js', array( 'jquery' ), FX_PBBASE_VERSION );

        /* Enqueue CSS & JS For Page Builder */
        wp_enqueue_style( 'fx-pbbase-admin', FX_PBBASE_URI. 'assets/admin-page-builder.css', array(), FX_PBBASE_VERSION );
        wp_enqueue_script( 'fx-pbbase-admin', FX_PBBASE_URI. 'assets/admin-page-builder.js', array( 'jquery', 'jquery-ui-sortable' ), FX_PBBASE_VERSION, true );
    }
}

Remember to use “jquery-ui-sortable” as dependency because we are going to make this rows sortable.

Now with a little CSS, this is what we have (actually look pretty good right? ):

page-builder-template-styled

Here is the CSS I use (work in progress):

/**
 * Page Builder Admin CSS
**/
#postdivrich{
    display:none;
}
#fx-page-builder{
    margin: 20px 0;
}
/* Rows */
.fxpb-rows{
    margin-bottom: 30px;
}
.fxpb-rows-message{
    border: 2px dashed #ccc;
    padding: 10px 20px;
    text-align: center;
}
/* Buttons */
.fxpb-actions{
    margin-bottom: 30px;
}
/* Row */
.fxpb-row{
    font-size: 16px;
    border: 1px solid #ccc;
    background: #fff;
    margin-bottom: 20px;
}
.fxpb-row-title{
}
.fxpb-row-title:after{
    content:".";display:block;height:0;clear:both;visibility:hidden;
}
.fxpb-handle{
    padding: 10px;
    border-right: 1px solid #ccc;
    cursor: grab;
}
.fxpb-row-title-text{
    padding: 10px;
    display: inline-block;
}
.fxpb-remove{
    padding: 10px;
    border-left: 1px solid #ccc;
    float: right;
    cursor: pointer;
}
.fxpb-remove:hover{
    background: red;
    color: #fff;
}
.fxpb-row-fields{
    border-top: 1px solid #ccc;
    padding: 10px;
}
.fxpb-row-fields:after{
    content:".";display:block;height:0;clear:both;visibility:hidden;
}
.fxpb-row-fields textarea{
    width: 100%;
    height: 100px;
}
/* 1 column template */
.fxpb-col-1{
}
/* 2 columns template */
.fxpb-col-2{
}
.fxpb-col-2 .fxpb-col-2-left{
    width: 49%;
    float: left;
}
.fxpb-col-2 .fxpb-col-2-right{
    width: 49%;
    float: right;
}

After all design done. Let’s hide the template area, and start with JavaScript:

/* Templates */
.fxpb-templates{
    display: none;
}

This is what we’ll have:

page-builder-template-final-styled

The JavaScript

The JavaScript (jQuery) is very simple. Let’s start with a blank canvas on document ready:

jQuery( document ).ready( function( $ ){

    /* Add JS Code Here... */

});

There are 4 main things we want this JS script to do:

  1. Adding the row
  2. Handling empty row message
  3. Delete row
  4. Make row sortable

Add New Row

The JS is a simple:

/* Add Row */
$( 'body' ).on( 'click', '.fxpb-add-row', function(e){
    e.preventDefault();

    /* Target the template. */
    var template = '.fxpb-templates > .fxpb-' + $( this ).attr( 'data-template' );

    /* Clone the template and add it. */
    $( template ).clone().appendTo( '.fxpb-rows' );

    /* Hide Empty Row Message */
    $( '.fxpb-rows-message' ).hide();
});

Each of our button in .fxpb-actions has data-template attributes:

<div class="fxpb-actions">
    <a href="#" class="fxpb-add-row button-primary button-large" data-template="col-1">Add 1 Column</a>
    <a href="#" class="fxpb-add-row button-primary button-large" data-template="col-2">Add 2 Columns</a>
</div><!-- .fxpb-actions -->

the value of this attribute is the identifier of what template to load. And in the JS I add the target template element as variable template.

First we get the target “template” from the button :

/* Target the template. */
var template = '.fxpb-templates > .fxpb-' + $( this ).attr( 'data-template' );

From the code above, the result for template variable are:

  • for Add 1 Column button this will return:
    .fxpb-templates > .fxpb-col-1
  • and for Add 2 Columns button this will return:
    .fxpb-templates > .fxpb-col-2

After we get the target element template class, we use jQuery to clone it and add it to .fxpb-rows .

/* Clone the template and add it. */
$( template ).clone().appendTo( '.fxpb-rows' );

Of course after that, we need to hide the default/empty message.

/* Hide Empty Row Message */
$( '.fxpb-rows-message' ).hide();

Delete Row

/* Delete Row */
$( 'body' ).on( 'click', '.fxpb-remove', function(e){
    e.preventDefault();

    /* Delete Row */
    $( this ).parents( '.fxpb-row' ).remove();
    
    /* Show Empty Message When Applicable. */
    if( ! $( '.fxpb-rows > .fxpb-row' ).length ){
        $( '.fxpb-rows-message' ).show();
    }
});

This is also, simple.

/* Delete Row */
$( this ).parents( '.fxpb-row' ).remove();

The code above simply remove the current row, when we click .fxpb-remove button. And then we show the default message if it’s the last row.

$( '.fxpb-rows > .fxpb-row' ).length

The code above will return true if there’s a row available in .fxpb-rows element.

Show/Hide Empty Row Message

/* Hide/Show Empty Row Message */
if( $( '.fxpb-rows > .fxpb-row' ).length ){
    $( '.fxpb-rows-message' ).hide();
}
else{
    $( '.fxpb-rows-message' ).show();
}

The code above intended to hide/show the empty message on page load. To make sure the default message not showing when we already add a row.

Make Row Sortable

Please make sure to add jquery-ui-sortable as dependency for the js script.

wp_enqueue_script( 'fx-pbbase-admin', FX_PBBASE_URI. 'assets/admin-page-builder.js', array( 'jquery', 'jquery-ui-sortable' ), FX_PBBASE_VERSION, true );

and this simple JS code will make the rows sortable and user can drag-and-drop rows to reorder.

/* Make Row Sortable */
$( '.fxpb-rows' ).sortable({
    handle: '.fxpb-handle',
    cursor: 'grabbing',
});

And that’s it. This is the result of this tutorial:

Download the Source Code:

Now the drag-and-drop is working, the UI to add and remove row is working, but nothing happen when we save the page.

The next tutorial is the fun part, I’m going to explain how to save all the rows data, the row order, validation,  etc.

Read Part #3 Tutorial

To get updates, follow my twitter @turtlepod.

Leave a Reply

Your email address will not be published. Required fields are marked *