How to Create Custom Elements Using Visual Composer API

Visual Composer Website Builder is a powerful drag-and-drop website builder for WordPress with tons of features and customization options that help design and create a website easily and effortlessly. Visual Composer features premium templates, elements, and extensions to help you create a professional website.

Visual Composer Website Builder has an API that allows anyone to develop custom elements for their site. This option opens up a lot of possibilities, so here are the main benefits of creating custom elements:

  • You are in charge of the element fully - from the design to the functionality;
  • You can create unique designs and work on custom projects;
  • API allows creating add-ons and in return earning some money;
  • You can integrate Visual Composer into your themes and include your custom elements.

We’re going to cover step-by-step instructions that will help you to create your own custom element.

This article contains:

Getting started

There are a few things you’ll need to take into account before start creating your first custom element.

Knowledge

In order to start developing an element, you should be familiar with the following technologies and skills.

  • WordPress;
  • HTML (JSX);
  • CSS;
  • JavaScript (ReactJS);
  • PHP;
  • Webpack;
  • Yarn;
  • Terminal.

Dev Environment

Since you’ll be developing an element for the Visual Composer plugin, you’ll need a local server with the latest WordPress installation on it and a Free or Premium version of the Visual Composer Website Builder plugin installed.

Requirements

To start developing custom Visual Composer elements following technical requirements should be met:

  • Node 8.0 or higher (recommended latest) installed;
  • PHP 5.4 or higher (recommended latest) installed.

For more information, visit Visual Composer Server requirements.

Code Editor

For the ease of editing, we recommend that you use special code editing tools, however, you can use any code editor or IDE that you find useful. Here is the list of some of the popular free code editors:

Terminal

Since we’ll be running terminal commands to build our element, you should be familiar with working with the terminal. MAC OS and Linux have built-in terminals. If you’re using Windows we recommend using the ConEmy console emulator. Also, some code editors and IDEs have built-in terminals.

Install example plugin

To make it easier for you, we’ve created an example plugin, that you can install and play around with. Follow the steps to install it and check if it works:

  1. Download, install, and activate the example plugin like you would any other WordPress plugin;
  2. Create a page with Visual Composer;
  3. Once in the editor, open Add Element panel and you should see a Text Test element appear;
  4. Hover over this element and you should see an Add-on tag on the preview image;
  5. Try adding the Text Test element to a page.

Edit example plugin files

Now that you’ve installed the plugin and checked that it works, you can make changes to it to get acquainted with the code and the element file structure.

  1. Locate example plugin folder in your WordPress installation under wp-content/plugins/;
  2. Open it in the code editor;
  3. You’ll see an elements folder inside the plugin folder;
  4. The elements folder contains three example elements;
  5. Each element has a specific structure;
  6. In order to see an element or to see the changes made to an element you have to build it;
  7. Run yarn install and yarn build commands inside the element folder to build an element;
  8. Use the yarn watch command to watch changes made to an element.

Once you’ve made the changes to an element and built it, refresh the editor page and try adding the element again.

Development

"To get started, you need to understand how the Visual Composer Website Builder Editor works. The Editor is WYSIWYG module based editor which has inner and public API to manage elements and components. The editor works with elements as React components. These components must be built with Webpack and enqueued via API hooks (WordPress actions)."

For this example, we’re going to create the Banner Element, that you can find in Visual Composer Hub.

Element

“The Visual Composer element is an independent component of the system which represents HTML based block with the ability to output media and dynamic content. Visual Composer offers a wide range of pre-defined content elements which can be downloaded from the Visual Composer Hub. Visual Composer API allows extending the list with your own elements – these elements will not be available in the Hub but will be instantly displayed in the ‘Add Element’ window.”

The Banner Element will allow a user to add images with links and display a random image every time the page loads. This element will include and have the ability to: 

  • Add images;
  • Add links to images;
  • Set image size;
  • Set image alignment;
  • Add custom class name and id;
  • Design options.

File Structure

To create custom element we’re going to use element boilerplate as a base for our element.

There are certain files required to build element:

 

  • manifest.json – describes element data, preview, thumbnail, description, category, and PHP files;
  • package.json – npm install/update files for required node modules;
  • Directory {tag}*:
    • cssMixins directory – cssMixin.pcss: a POST CSS file that holds mixin for custom generated style;
    • public directory – may contain custom CSS, JS, and images for public view
    • index.js – the main file, build with Webpack;
    • component.jsReactJs component contains Visual Composer Editor component. This component is used only in the editor;
    • settings.json – describes element attributes;
    • *.php files – required PHP files (must be described in ../manifest.json);
    • editor.css – CSS files which will be used only in the editor;
    • style.css – CSS files which will be used on the frontend to display content.

Just like for the example plugin we need to create our plugin's new folder under the wp-content/plugins/ folder, let’s name it banner-element.

Inside the banner-element folder, we need to create an index.php file of our plugin. Next, we need to create an elements folder which will store plugins elements. Then we download the element boilerplate zip file and extract it to the elements folder. Rename vcwb-element-boilerplate to bannerElement.

NOTE: folder name bannerElement will act as a tag name for our element and will be used throughout the code.

In the end, the structure will look like this:

Custom element folder structure

Code

index.php file

First, let’s edit the index.php file.

We add a comment describing our plugin. Next a generic code to prevent the access to the index.php file. Then we’ll use the following hook for API calls. This is the code from the example plugin. Now we need to change the element name in the $elementsToRegister array, so the complete code will look as follows:

<?php
/**
Plugin Name: Banner Element
Plugin URI: https://visualcomposer.com
Description: Add image banners to your page that will load randomly - only one banner will be loaded at the time.
Version: 1.0
Author: Visual Composer
Author URI: https://visualcomposer.com
*/

// don't load directly
if (!defined('ABSPATH')) {
die('-1');
}
add_action(
/**
* @param $api \VisualComposer\Modules\Api\Factory
*/
'vcv:api',
function ($api) {
$elementsToRegister = [
'bannerElement',
];
$pluginBaseUrl = rtrim(plugins_url(basename(__DIR__)), '\\/');
/** @var \VisualComposer\Modules\Elements\ApiController $elementsApi */
$elementsApi = $api->elements; foreach ($elementsToRegister as $tag) { $manifestPath = __DIR__ . '/elements/' . $tag . '/manifest.json'; $elementBaseUrl = $pluginBaseUrl . '/elements/' . $tag; $elementsApi->add($manifestPath, $elementBaseUrl); } } );

Once this is done, a new plugin should appear in the plugins page in WordPress dashboard.

Banner element plugin


Next, we need to edit folder and files in the element boilerplate. We’ve already renamed the vcwb-element-boilerplate folder and the same name nested folder to bannerElement. So the structure looks like:

Folder name


Inside bannerElement folder there’s another bannerElement folder which contains element files and folders. Let’s manage those.

Manage element files


For this element we won’t use any cssMixins, so we can delete the cssMixins folder. The public folder will contain styles, scripts, and images, so let’s change the default images. We’ll need new preview and thumbnail images for our element.

Thumbnail:

Banner element thumbnail


Preview:

Banner element preview


Also, we’ll need a couple of default images to go.

Banner image 1
Banner default image 2


So now the public folder will look like this.

Banner element public folder

manifest.json file

The manifest.json file describes element data, preview and thumbnail images, element category and PHP files. We need to specify element tag - bannerElement, which is the first property of the elements object. Element name will be - “Banner Element” and it goes under “name” property, and the description will be - “Add image banners to your page that will load randomly - only one banner will be loaded at the time.” and it goes under “metaDescription” property. The category will be “Misc”. You can find more about categories in the official API docs. In the end, the manifest.json file will look like this:

{
 "elements": {
   "bannerElement": {
     "settings": {
       "name": "Banner Element",
       "metaThumbnailUrl": "[publicPath]/banner-element-thumbnail.png",
       "metaPreviewUrl": "[publicPath]/banner-element-preview.png",
       "metaDescription": "Add image banners to your page that will load randomly - only one banner will be loaded at the time."
     }
   }
 },
 "categories": {
   "Misc": {
     "elements": [
       "bannerElement"
     ]
   }
 }
}

settings.json file

Now let’s get to visualizing our element. First, we need to specify what kind of settings element will have. For this, we’ll edit the settings.json file. In settings.json file data stored in an object, each object property is an attribute. You can get acquainted with all the possible attributes and attribute options in the API docs.

It’s a good practice to start with a tag attribute so that when you open a file you recognize the element right away. Next, we’ll specify the relatedTo attribute, which defines the relationship to drag and drop groups, we’ll set it equal to General. Note that they’re both protected attributes, so it means they are not accessible within an element.

"tag": {
  "access": "protected",
  "type": "string",
  "value": "bannerElement"
},
"relatedTo": {
  "type": "group",
  "access": "protected",
  "value": [
    "General"
  ]
},

The following attribute names speak for themselves:

  • images will use attachimage type attribute with label, description, multiple, and url options;
  • size will use string type attribute with label, description options;
  • alignment will use buttonGroup type attribute with label, values options;
  • customClass will use string type attribute with label, description options;
  • metaCustomID will use string type attribute with label, description options;
  • designOptions will use string type attribute with label option;

Then, we specify the order of the attributes in the Edit Form panel of the element in the editFormTab1 attribute. Specify the order of sections (in this case section are represented by editFormTab1 and designOptions attributes) in the Edit Form panel of the element in the metaEditFormTabs attribute. Note that these two attributes are also protected.

"editFormTab1": {
   "type": "group",
   "access": "protected",
   "value": [
     "images",
     "size",
     "alignment",
     "metaCustomId",
     "customClass"
   ],
   "options": {
     "label": "General"
   }
 },
 "metaEditFormTabs": {
   "type": "group",
   "access": "protected",
   "value": [
     "editFormTab1",
     "designOptions"
   ]
 },

Lastly, we need to specify the path to our custom JS file that will be loaded only on View Page (public page). The metaPublicJs attribute will handle this. Note this attribute is also protected.

"metaPublicJs": {
  "access": "protected",
  "type": "string",
  "value": {
    "libraries": [
      {
        "libPaths": [
          "public/dist/bannerElement.min.js"
        ]
      }
    ]
  }
}

So, in the end, our settings.json file will look like this:

{
 "tag": {
   "access": "protected",
   "type": "string",
   "value": "bannerElement"
 },
 "relatedTo": {
   "type": "group",
   "access": "protected",
   "value": [
     "General"
   ]
 },
 "images": {
   "type": "attachimage",
   "access": "public",
   "value": [
     "banner-img-01.png",
     "banner-img-02.png"
   ],
   "options": {
     "label": "Images",
     "description": "Only one image will be loaded at the time.",
     "multiple": true,
     "url": true
   }
 },
 "size": {
   "type": "string",
   "access": "public",
   "value": "full",
   "options": {
     "label": "Size",
     "description": "Enter image size (Example: 'thumbnail', 'medium', 'large', 'full' or other sizes defined by theme). Alternatively enter size in pixels (Example: 200x100 (Width x Height))."
   }
 },
 "alignment": {
   "type": "buttonGroup",
   "access": "public",
   "value": "left",
   "options": {
     "label": "Alignment",
     "values": [
       {
         "label": "Left",
         "value": "left",
         "icon": "vcv-ui-icon-attribute-alignment-left"
       },
       {
         "label": "Center",
         "value": "center",
         "icon": "vcv-ui-icon-attribute-alignment-center"
       },
       {
         "label": "Right",
         "value": "right",
         "icon": "vcv-ui-icon-attribute-alignment-right"
       }
     ]
   }
 },
 "customClass": {
   "type": "string",
   "access": "public",
   "value": "",
   "options": {
     "label": "Extra class name",
     "description": "Add an extra class name to the element and refer to it from Custom CSS option."
   }
 },
 "metaCustomId": {
   "type": "customId",
   "access": "public",
   "value": "",
   "options": {
     "label": "Element ID",
     "description": "Apply unique Id to element to link directly to it by using #your_id (for element id use lowercase input only)."
   }
 },
 "designOptions": {
   "type": "designOptions",
   "access": "public",
   "value": {},
   "options": {
     "label": "Design Options"
   }
 },
 "editFormTab1": {
   "type": "group",
   "access": "protected",
   "value": [
     "images",
     "size",
     "alignment",
     "metaCustomId",
     "customClass"
   ],
   "options": {
     "label": "General"
   }
 },
 "metaEditFormTabs": {
   "type": "group",
   "access": "protected",
   "value": [
     "editFormTab1",
     "designOptions"
   ]
 },
 "metaPublicJs": {
   "access": "protected",
   "type": "string",
   "value": {
     "libraries": [
       {
         "libPaths": [
           "public/dist/bannerElement.min.js"
         ]
       }
     ]
   }
 }
}

Note: The order of the attributes doesn’t matter, we just showed you how we do it.

index.js file

This is the main JS file where we include settings.json, component.js file and CSS files. We start off by importing vcCake library, for now, all you need to know about it is that it will handle communication between the modules in the Visual Composer system with the help of services and storages. Right after that, we import our element BannerElement component class.

import vcCake from 'vc-cake'
import BannerElement from './component'

Then we define vcvAddElement which will add our element to the system.

const vcvAddElement = vcCake.getService('cook').add

And pass several arguments to it, like settings.json, imported element component, and CSS files.

For CSS there are three available properties css, editorCss, and cssMixins.

  • css property specifies the main CSS file with element styles;
  • editorCss property specifies the CSS file which will be loaded only in the editor;
  • cssMixins property can specify one or multiple POST CSS files which will be used by the system to generate element styles.

For our element, we will only need css and editorCss properties.
So in the end, we get:

import vcCake from 'vc-cake'
import BannerElement from './component'
 
const vcvAddElement = vcCake.getService('cook').add
 
vcvAddElement(
 require('./settings.json'),
 // Component callback
 (component) => {
   component.add(BannerElement)
 },
 // css settings // css for element
 {
   'css': require('raw-loader!./styles.css'),
   'editorCss': require('raw-loader!./editor.css')
 }
)

component.js file

This file is the React component and contains element class. Every JS code we write here will run only in the editor.

We will be using ES6 syntax to write code, it will be transpiled by Babel. We recommend using code style tools to keep your code properly formatted and avoid bugs. For our projects, we use the Standard tool.

Here we also import vcCake library in the beginning along with React library.

import React from 'react'
import vcCake from 'vc-cake'

Define two vcCake services: vcvApi and renderProcessor.

const vcvAPI = vcCake.getService('api')
const renderProcessor = vcCake.getService('renderProcessor')

You’ll be using vcvAPI for element component all of the time, while renderProcessor is quite a unique service and is used in just a few elements. As the name states, it is used to render HTML, asynchronously. We will use it to render images in the editor since we need to have the ability to frequently change image sizes.

Then we create our BannerElement class by extending vcvAPI.elementComponent. It will contain a lot of custom code, mostly related to rendering images in the editor.

export default class BannerElement extends vcvAPI.elementComponent {
  ...
}

The render is the React class method where you’ll be writing most of your code for the most part. If you’re not familiar with React, this method will return a markup for the element. Here we can get the values of the element attributes, make decisions based on conditions, define classes for CSS and write markup in JSX.

The first thing we do is destruct this.props object to get the id, atts and editor variables.

  • id is the unique ID of the element and is used to recognize the element by the system;
  • atts is the attributes of the element, the ones that are set in settings.json file;
  • editor object holds data attributes necessary for the editor (for drag and drop).
const { id, atts, editor } = this.props

We also destruct the atts prop to get the attribute values.

const { images, size, alignment, metaCustomId, customClass } = atts

Define variables for classes and custom props (like custom element ID).

let containerClasses = 'vce-banner-element-container vce'
let bannerClasses = 'vce-banner-element'
let linkClasses = 'vce-banner-element-link'
let imageClasses = 'vce-banner-element-image'
let bannerProps = {}
let customImageProps = {}

Based on the value of the attributes we add classes and props.

if (alignment) {
  containerClasses += ` vce-banner-element--align-${alignment}`
}

if (typeof customClass === 'string' && customClass) {
  containerClasses += ` ${customClass}`
}

if (metaCustomId) {
  bannerProps.id = metaCustomId
}

We then map our images into an array of elements and store in the variable. We define CustomTag variable and set it equal to span tag to further check whether an image has a URL selected if so, we change it to an anchor tag.

imgSrc variable is equal to an element built-in method getImageUrl that returns a string with a path to an image.

All of our images will be rendered, this needs to be done because the size of the image can be calculated once it is in the DOM. For this, we will use a helper element with a class of vcvhelper. Since only one image needs to be displayed we will hide the rest images with CSS.

let Images = images.map((image, index) => {
  let CustomTag = 'span'
  let customProps = {}
  const imageProperty = `image-${index}`
  const imgSrc = this.getImageUrl(image)
  customImageProps[ 'data-img-src' ] = imgSrc
  customImageProps[ 'alt' ] = image && image.alt ? image.alt : ''
  customImageProps[ 'title' ] = image && image.title ? image.title : ''

  if (image && typeof image === 'object' && image.constructor === Object && image.link) {
    let { url, title, targetBlank, relNofollow } = image.link
    CustomTag = 'a'
    customProps = {
      'href': url,
      'title': title,
      'target': targetBlank ? '_blank' : undefined,
      'rel': relNofollow ? 'nofollow' : undefined
    }
  }

  customProps.key = `customProps:${id}-${imgSrc}-${size}`

  const shortcodeOptions = {
    props: customImageProps,
    classes: imageClasses,
    isDefaultImage: !(image && image.id),
    src: imgSrc
  }
return (
<CustomTag className={linkClasses} {...customProps} key={`vce-banner-element-image-${index}-${id}`}>
<span
className={`${imageClasses} vcvhelper`}
{...customImageProps}
data-vcvs-html={this.getImageShortcode(shortcodeOptions, imageProperty)}
>
<canvas data-image-property={imageProperty} />
</span>
</CustomTag>
) })

It’s important to understand that the editor property (that we assigned to a variable) should be set on the element's outermost container. We do it by using the spread operator.

It is recommended to set the id attribute with the id property on the outermost container as well, however, that is not always mandatory. For this element, we will set it on the main container.

To get the Design Options we use another element build-in method this.applyDO(‘all’). It accepts a string argument that may contain different data. We will store it in a variable and use the string ‘all’ argument to get all of the Design Options attributes.

const doAll = this.applyDO('all')

We then use the spread operator to assign them to a div tag with bannerClasses.

In the end, the whole completed component will look as follows:

import React from 'react'
import vcCake from 'vc-cake'
import { isEqual } from 'lodash'

const vcvAPI = vcCake.getService('api')
const renderProcessor = vcCake.getService('renderProcessor')

export default class BannerElement extends vcvAPI.elementComponent {
  promise = null
  canvases = {}

  static drawImageProp (ctx, img, x, y, w, h, offsetX, offsetY) {
    if (arguments.length === 2) {
      x = y = 0
      w = ctx.canvas.width
      h = ctx.canvas.height
    }

    // default offset is center
    offsetX = typeof offsetX === 'number' ? offsetX : 0.5
    offsetY = typeof offsetY === 'number' ? offsetY : 0.5

    // keep bounds [0.0, 1.0]
    if (offsetX < 0) {
      offsetX = 0
    }
    if (offsetY < 0) {
      offsetY = 0
    }
    if (offsetX > 1) {
      offsetX = 1
    }
    if (offsetY > 1) {
      offsetY = 1
    }

    let iw = img.width
    let ih = img.height
    let r = Math.min(w / iw, h / ih)
    let nw = iw * r // new prop. width
    let nh = ih * r // new prop. height
    let cx
    let cy
    let cw
    let ch
    let ar = 1

    // decide which gap to fill
    if (Math.round(nw) < w) {
      ar = w / nw
    }
    if (Math.round(nh) < h) {
      ar = h / nh
    }

    nw *= ar
    nh *= ar

    // calc source rectangle
    cw = iw / (nw / w)
    ch = ih / (nh / h)

    cx = (iw - cw) * offsetX
    cy = (ih - ch) * offsetY

    // make sure source rectangle is valid
    if (cx < 0) {
      cx = 0
    }
    if (cy < 0) {
      cy = 0
    }
    if (cw > iw) {
      cw = iw
    }
    if (ch > ih) {
      ch = ih
    }

    // make canvas high quality
    ctx.imageSmoothingQuality = 'high'

    // fill image in dest. rectangle
    ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h)
  }

  static images = {}

  constructor (props) {
    super(props)

    this.state = {
      imgElements: null
    }

    this.setImage = this.setImage.bind(this)
    this.setImageState = this.setImageState.bind(this)
    this.setError = this.setError.bind(this)
  }

  componentDidMount () {
    this.promise = new window.Promise((resolve, reject) => {
      this.resolve = resolve
      this.setImage(this.props)
    })
    renderProcessor.add(this.promise)
  }

  componentWillUnmount () {
    const images = Object.keys(BannerElement.images)
    images.forEach((image) => {
      BannerElement.images[image] && BannerElement.images[image].removeEventListener('load', this.setImageState)
      BannerElement.images[image] && BannerElement.images[image].removeEventListener('error', this.setError)
    })
  }

  componentDidUpdate (prevProps) {
    if (!isEqual(prevProps.atts, this.props.atts)) {
      this.setImage(this.props)
    }
  }

  parseSize (size, naturalWidth, naturalHeight) {
    let crop = true
    if (typeof size === 'string') {
      size = size.replace(/\s/g, '').replace(/px/g, '').toLowerCase().split('x')
    } else if (typeof size === 'object') {
      crop = size.crop
      size = [ size.width, size.height ]
    }

    naturalWidth = parseInt(naturalWidth)
    naturalHeight = parseInt(naturalHeight)

    const cropHorizontal = parseInt(size[ 0 ]) < naturalWidth
    const cropVertical = parseInt(size[ 1 ]) < naturalHeight

    if (crop) {
      size[ 0 ] = parseInt(size[ 0 ]) < naturalWidth ? parseInt(size[ 0 ]) : naturalWidth
      size[ 1 ] = parseInt(size[ 1 ]) < naturalHeight ? parseInt(size[ 1 ]) : naturalHeight
    } else {
      size[ 0 ] = cropHorizontal ? parseInt(size[ 0 ]) : naturalWidth
      size[ 1 ] = cropVertical ? parseInt(size[ 1 ]) : naturalHeight

      if (cropHorizontal && !cropVertical) {
        const prop = size[ 0 ] / naturalWidth
        size[ 1 ] = parseInt(naturalHeight * prop)
      }

      if (cropVertical && !cropHorizontal) {
        const prop = size[ 1 ] / naturalHeight
        size[ 0 ] = parseInt(naturalWidth * prop)
      }

      if (cropVertical && cropHorizontal) {
        if (naturalHeight < naturalWidth) {
          const prop = size[ 0 ] / naturalWidth
          size[ 1 ] = parseInt(naturalHeight * prop)
        } else {
          const prop = size[ 1 ] / naturalHeight
          size[ 0 ] = parseInt(naturalWidth * prop)
        }
      }
    }

    size = {
      width: size[ 0 ],
      height: size[ 1 ]
    }

    return size
  }

  checkRelatedSize (size) {
    let relatedSize = null
    if (window.vcvImageSizes && window.vcvImageSizes[ size ]) {
      relatedSize = window.vcvImageSizes[ size ]
    }
    return relatedSize
  }

  getSizes (atts, img) {
    let { size } = atts
    size = size.replace(/\s/g, '').replace(/px/g, '').toLowerCase()

    let parsedSize = ''

    if (size.match(/\d+(x)\d+/)) {
      parsedSize = this.parseSize(size, img.width, img.height)
    } else {
      parsedSize = this.checkRelatedSize(size)

      if (parsedSize) {
        parsedSize = this.parseSize(parsedSize, img.width, img.height)
      } else {
        parsedSize = this.parseSize({ width: img.width, height: img.height }, img.width, img.height)
      }
    }

    return {
      width: parsedSize.width,
      height: parsedSize.height
    }
  }

  setImage (props) {
    const { images } = props.atts
    let newState = {}
    images.forEach((image, index) => {
      const imgSrc = this.getImageUrl(image)
      const imageProperty = `image-${index}`

      BannerElement.images[imageProperty] && BannerElement.images[imageProperty].removeEventListener('load', this.setImageState)
      BannerElement.images[imageProperty] && BannerElement.images[imageProperty].removeEventListener('error', this.setError)

      BannerElement.images[imageProperty] = new window.Image()

      BannerElement.images[imageProperty].addEventListener('load', this.setImageState.bind(this, index))
      BannerElement.images[imageProperty].addEventListener('error', this.setError)

      if (imgSrc) {
        BannerElement.images[imageProperty].src = imgSrc
      } else {
        this.setError()
      }

      if (!imgSrc) {
        newState[imageProperty] = {
          imgIndex: null,
          imgElement: null,
          parsedWidth: null,
          parsedHeight: null,
          naturalWidth: null,
          naturalHeight: null
        }
      }
    })
    this.setState({ imgElements: newState })
  }

  setImageState (index, e) {
    let { imgElements } = this.state
    const img = e.currentTarget
    const sizes = this.getSizes(this.props.atts, img)
    const imageProperty = `image-${index}`

    imgElements[imageProperty] = {
      imgIndex: index,
      imgElement: e.currentTarget,
      parsedWidth: sizes.width,
      parsedHeight: sizes.height,
      naturalWidth: img.width,
      naturalHeight: img.height
    }
    this.setState({
      imgElements: imgElements
    }, () => {
      this.resolve && this.resolve(true)
    })
  }

  setError () {
    this.resolve && this.resolve(false)
  }

  resizeImage () {
    const { images } = this.props.atts
    for (let i = 0; i < images.length; i++) {
      const imageProperty = `image-${i}`
      if (!this.canvases.hasOwnProperty(imageProperty) || !this.state.imgElements[imageProperty]) {
        continue
      }
      const { imgElement, parsedWidth, parsedHeight, naturalWidth, naturalHeight } = this.state.imgElements[imageProperty]
      const ctx = this.canvases[imageProperty].getContext('2d')
      if (!imgElement) {
        ctx.clearRect(0, 0, parsedWidth, parsedHeight)
        continue
      }
      this.canvases[imageProperty].width = parsedWidth
      this.canvases[imageProperty].height = parsedHeight
      this.canvases[imageProperty].naturalWidth = naturalWidth
      this.canvases[imageProperty].naturalHeight = naturalHeight

      BannerElement.drawImageProp(ctx, imgElement, 0, 0, parsedWidth, parsedHeight, 0.5, 0.5)
    }
  }

  getImageShortcode (options, imageProperty) {
    const { props, classes, isDefaultImage, src } = options
    const image = this.state.imgElements && this.state.imgElements[imageProperty]
    const width = image && image.parsedWidth ? image.parsedWidth : 0
    const height = image && image.parsedHeight ? image.parsedHeight : 0
    let shortcode = `[vcvSingleImage class="${classes}" style="position: absolute; visibility: hidden; left: 0; z-index: -1;" data-width="${width}" data-height="${height}" src="${src}" data-img-src="${props[ 'data-img-src' ]}" alt="${props.alt}" title="${props.title}"`

    if (isDefaultImage) {
      shortcode += ' data-default-image="true"'
    }

    shortcode += ']'

    return shortcode
  }

  render () {
    let { id, atts, editor } = this.props
    let { images, size, alignment, metaCustomId, customClass } = atts
    let containerClasses = 'vce-banner-element-container vce'
    let bannerClasses = 'vce-banner-element'
    let linkClasses = 'vce-banner-element-link'
    let imageClasses = 'vce-banner-element-image'
    let bannerProps = {}
    let customImageProps = {}

    if (alignment) {
      containerClasses += ` vce-banner-element--align-${alignment}`
    }

    if (typeof customClass === 'string' && customClass) {
      containerClasses += ` ${customClass}`
    }

    if (metaCustomId) {
      bannerProps.id = metaCustomId
    }

    let Images = images.map((image, index) => {
      let CustomTag = 'span'
      let customProps = {}
      const imageProperty = `image-${index}`
      const imgSrc = this.getImageUrl(image)
      customImageProps[ 'data-img-src' ] = imgSrc
      customImageProps[ 'alt' ] = image && image.alt ? image.alt : ''
      customImageProps[ 'title' ] = image && image.title ? image.title : ''

      if (image && typeof image === 'object' && image.constructor === Object && image.link) {
        let { url, title, targetBlank, relNofollow } = image.link
        CustomTag = 'a'
        customProps = {
          'href': url,
          'title': title,
          'target': targetBlank ? '_blank' : undefined,
          'rel': relNofollow ? 'nofollow' : undefined
        }
      }

      customProps.key = `customProps:${id}-${imgSrc}-${size}`

      const shortcodeOptions = {
        props: customImageProps,
        classes: imageClasses,
        isDefaultImage: !(image && image.id),
        src: imgSrc
      }

      return (
        <CustomTag className={linkClasses} {...customProps} ref='imageContainer' key={`vce-banner-element-image-${index}-${id}`}>
          <span className={`${imageClasses} vcvhelper`}
            {...customImageProps}
            data-vcvs-html={this.getImageShortcode(shortcodeOptions, imageProperty)}>
            <canvas ref={(canvas) => { this.canvases[imageProperty] = canvas }} />
          </span>
        </CustomTag>
      )
    })

    const doAll = this.applyDO('all')
    this.resizeImage()

    return <div className={containerClasses} id={'el-' + id} {...editor}>
      <div className={bannerClasses} {...bannerProps} {...doAll}>
        {Images}
      </div>
    </div>
  }
}

editor.css file

Since this file is used only in the editor we’ll use it to set some styles specifically for the editor view.

First, we need to set the min-height property and set it to 1em, to make the element visible for hover in case no images are selected.

.vce-banner-element {
 min-height: 1em;
}

Also, we need to style in editor images which are basically inside canvases.

.vce-banner-element canvas {
 max-width: 100%;
 vertical-align: top;
}

.vce-banner-element .vcvhelper {
 display: inline-block;
 vertical-align: top;
}

And in the end in the editor, we will show only the first image. In the View Page the image will be displayed randomly by script.

.vce-banner-element-link {
 position: absolute;
 visibility: hidden;
}

.vce-banner-element-link:first-child {
 position: relative;
 visibility: visible;
}

styles.css file

Here we’re just going to set general styles, nothing fancy. Since the code is very simple and the volume is minimal we’re not going to use any preprocessors for this, only pure CSS.

Note: If you’re willing to use any preprocessor, you should create a separate folder for that in the bannerElement/bannerElement/public/ folder e.g.: bannerElement/bannerElement/public/sass.

And then compile the main preprocessor file to the styles.css file.

For the anchor tag, we’re going to reset some styles, because some of the WordPress themes often custom style links.

a.vce-banner-element-link {
 color: transparent;
 border-bottom: 0;
 text-decoration: none;
 box-shadow: none;
}

The complete code looks like this:

.vce-banner-element {
 position: relative;
 display: inline-block;
}
 
a.vce-banner-element-link,
span.vce-banner-element-link {
 color: transparent;
 border-bottom: 0;
 text-decoration: none;
 box-shadow: none;
}
 
a.vce-banner-element-link:hover,
a.vce-banner-element-link:focus {
 text-decoration: none;
 box-shadow: none;
 border-bottom: 0;
}
 
.vce-banner-element-link {
 display: inline-block;
 vertical-align: top;
 line-height: 1;
 max-width: 100%;
}
 
.vce-single-image-wrapper img {
 vertical-align: top;
 max-width: 100%;
}
 
.vce-banner-element--align-center {
 text-align: center;
}
 
.vce-banner-element--align-right {
 text-align: right;
}
 
.vce-banner-element--align-left {
 text-align: left;
}

webpack.config.js file (in the public folder)

In a public folder, we’ll create a webpack.config.js file.

webpack.config.js file

It is a very simple config file used to list input and output JS file for public use. Here we’re going to write the name and the path of our public JS file, for webpack to minimize. The code is as follows:

module.exports = {
 entry: {
   'bannerElement': [ './js/bannerElement.js' ]
 },
 output: {
   filename: '[name].min.js'
 }
}

js folder and bannerElement.js file (in public folder)

Inside the public folder, we’ll create a js folder with bannerElement.js file.

js folder with bannerElement

This file will contain a small script for View page, which will randomly pick and display an image.

Note: This script will also run in the editor.

We’ll start with a self-invoking document.ready() function.

(function ($) {
  ...
})(window.jQuery)

Then we’ll subscribe to a special event, which happens when the page loads.

(function ($) {
  window.vcv.on('ready', function () {
    ...
  })
})(window.jQuery)

The code that is run on the event does following:

  1. selects all banner elements;
  2. checks if it is not the editor by checking for .vcvhelper class which is editor specific only;
  3. checks if there’s more than one image (why execute if more than one image, duh);
  4. hides all images by setting inline style;
  5. picks a random image and removes its style attribute.

The end code looks as follows:

(function ($) {
 window.vcv.on('ready', function () {
   var banners = $('.vce-banner-element')
   if (banners.length) {
     banners.each(function () {
       var banner = $(this)
       var images = banner.find('.vce-banner-element-image')
       if (images.hasClass('vcvhelper')) {
         return
       }
       if (images.length > 1) {
         images.css({
           'position': 'absolute',
           'visibility': 'hidden'
         })
         var randomIndex = Math.floor((Math.random() * images.length))
         var randomImage = $(images[ randomIndex ])
         randomImage.removeAttr('style')
       }
     })
   }
 })
})(window.jQuery)
 

Building an element

Whether you’re creating a new element or editing existing, every time you make a change in files, you need to build an element.
In the terminal, we’ll enter into our element’s folder.

cd banner-element/elements/bannerElement

Run yarn install command. Once npm is installed we’re able to build our element using yarn build. You can notice the change in folder structure:

Change in folder structure

Also, we can run yarn watch command to watch for file changes and automatically build an element. All available webpack commands for building an element can be found in API.

Once this is done we also need to build our public JS file bannerElement.js.

Again, in the terminal, we need to enter the public folder inside the bannerElement folder.

cd banner-element/elements/bannerElement/bannerElement/public

Once there run the following command:

../../node_modules/.bin/webpack --config=webpack.config.js -p

It will create a minified version of the original bannerElement.js file.

Minified version of bannerElement.js


Note: Everytime you make changes to a JS file in the public folder don’t forget to build it. Or use a command to constantly watch changes to a file and build it:

../../node_modules/.bin/webpack --config=webpack.config.js -p --watch

Testing an element

After we’ve made all the changes and have built our element, now’s the time to test it. Let's activate our plugin in the Plugins page of the WordPress admin dashboard.

Activate banner element plugin


Once activated, we open a page with Visual Composer. Click on the Add Element button in the navbar and see if we can spot our element, ah there it is.

Add banner element


Click on it to see if it adds to a page.

Edit element window


Success! Now we can add custom images, links, set size and alignment. Save and view the page to make sure it works.

Create Your Own Custom Elements

As you can see, Visual Composer gives you the power to be responsible for all aspects of the element. And now, that you've been through this guide, just follow the steps and you'll be creating your own elements and addons in no time. 

Visit our API documentation to get more information that will help you along the way!