Skip to main content

Server Side Rendering Element

Objective

The goal of this tutorial is to help you create server side rendering element on your module.

If you are developing module which its element is rendered in server (eg. Blog Module, Portfolio Module, etc), you will need to incorporate server side rendering element on your module. Unlike Divi 4 where server side rendering mechanism is handled by computed_callback and auto configured wp_ajax callback, Divi 5 offer more flexibility by handling server side rendering element via WordPress' REST API and React custom hook.

In this tutorial, we will render unordered list of recent post below content area on Simple Quick Module. The list will be rendered on Visual Builder and Frontend. Additionally, a field to modify amount of posts to be displayed will be added on module settings.

Here's how it looks like:

Server Side Rendering Element, Before/After

Prerequisites

This tutorial assumes that you have read, followed, and completed:

How It Works

Here's how it will be done:

  1. Create PHP method that will render unordered list of recent posts. This method will accept parameter that decides how many posts will be rendered inside the unordered list. Let's call this method render_recent_posts().
  2. Implement render_recent_posts() on Frontend render callback to display it on frontend.
  3. Set default value on module.json. This way if the module attribute doesn't exist, fallback value will be used.
  4. Update webpack configuration to include react and react-dom as externals.
  5. Use useFetch() hook to fetch the value from REST API endpoint then render it on Simple Quick Module's visual builder component.
  6. Added field on module settings' content panel to modify module attribute that control number of posts that will be displayed on the module
  7. Generate the build output to update Visual Builder script.

Write The Code

NOTE: See the highlighted part to see what's being modified.

Method for rendering recent posts & render it on frontend

To do this, we'll modify D5TUTSimpleQuickModule class.

server/index.php
<?php

namespace D5TUTSimpleQuickModule;

if ( ! defined( 'ABSPATH' ) ) {
die( 'Direct access forbidden.' );
}

require_once ABSPATH . 'wp-content/themes/Divi/includes/builder-5/server/Framework/DependencyManagement/Interfaces/DependencyInterface.php';

use ET\Builder\Framework\DependencyManagement\Interfaces\DependencyInterface;
use ET\Builder\Framework\Utility\HTMLUtility;
use ET\Builder\FrontEnd\Module\Style;
use ET\Builder\Packages\Module\Module;
use ET\Builder\Packages\Module\Options\Element\ElementClassnames;
use ET\Builder\Packages\ModuleLibrary\ModuleRegistration;

/**
* Class that handle "Simple Quick Module" module output in frontend.
*/
class D5TutorialSimpleQuickModule implements DependencyInterface {
/**
* Register module.
* `DependencyInterface` interface ensures class method name `load()` is executed for initialization.
*/
public function load() {
// Register module.
add_action( 'init', [ self::class, 'register_module' ] );
}

/**
* Register module.
*/
public static function register_module() {
// Path to module metadata that is shared between Frontend and Visual Builder.
$module_json_folder_path = dirname( __DIR__, 1 ) . '/visual-builder/src';

ModuleRegistration::register_module(
$module_json_folder_path,
[
'render_callback' => [ self::class, 'render_callback' ],
]
);
}

/**
* Render module style.
*/
public static function module_styles( $args ) {
$attrs = $args['attrs'] ?? [];
$elements = $args['elements'];

Style::add(
[
'id' => $args['id'],
'name' => $args['name'],
'orderIndex' => $args['orderIndex'],
'storeInstance' => $args['storeInstance'],
'styles' => [
// Module.
$elements->style(
[
'attrName' => 'module',
'styleProps' => [
'disabledOn' => [
'disabledModuleVisibility' => $args['settings']['disabledModuleVisibility'] ?? null,
],
],
]
),
// Title.
$elements->style(
[
'attrName' => 'title',
]
),
// Content.
$elements->style(
[
'attrName' => 'content',
]
),
],
]
);
}

/**
* Render module script data.
*/
public static function module_script_data( $args ) {
$elements = $args['elements'];

// Element Script Data Options.
$elements->script_data(
[
'attrName' => 'module',
]
);
}

/**
* Render module classnames.
*/
public static function module_classnames( $args ) {
$classnames_instance = $args['classnamesInstance'];
$attrs = $args['attrs'];

// Module.
$classnames_instance->add(
ElementClassnames::classnames(
[
'attrs' => $attrs['module']['decoration'] ?? [],
]
)
);
}

/**
* Render module HTML output.
*/
public static function render_callback( $attrs, $content, $block, $elements ) {
// Title.
$title = $elements->render(
[
'attrName' => 'title',
]
);

// Content.
$content = $elements->render(
[
'attrName' => 'content',
]
);

// Render list of recent posts and its div wrapper.
$recent_posts = HTMLUtility::render(
[
'tag' => 'div',
'attributes' => [
'class' => 'd5-tut-simple-quick-module-recent-posts',
],
'childrenSanitizer' => 'et_core_esc_previously',
'children' => self::render_recent_post(
[
'postsNumber' => $attrs['recentPosts']['innerContent']['desktop']['value']['postsNumber'],
]
),
]
);

// Module Inner.
// Essentially, this is the module content.
// Were wrapping the title and content in a div with class `et_pb_module_inner`.
$module_inner = HTMLUtility::render(
[
'tag' => 'div',
'attributes' => [
'class' => 'et_pb_module_inner',
],
'childrenSanitizer' => 'et_core_esc_previously',

// Include list of recent posts along with title and content.
'children' => $title . $content . $recent_posts,
]
);

// This are the module elements that will be rendered in the frontend.
$module_elements = $elements->style_components(
[
'attrName' => 'module',
]
);

// This are the children of the module container, which are the module elements and the module inner.
$module_container_children = $module_elements . $module_inner;

return Module::render(
[
// FE only.
'orderIndex' => $block->parsed_block['orderIndex'],
'storeInstance' => $block->parsed_block['storeInstance'],

// VB equivalent.
'attrs' => $attrs,
'elements' => $elements,
'id' => $block->parsed_block['id'],
'moduleClassName' => 'd5_tut_simple_quick_module',
'name' => $block->block_type->name,
'classnamesFunction' => [ self::class, 'module_classnames' ],
'moduleCategory' => $block->block_type->category,
'stylesComponent' => [ self::class, 'module_styles' ],
'scriptDataComponent' => [ self::class, 'module_script_data' ],
'children' => $module_container_children,
]
);
}

/**
* Return unordered list of recent posts.
*/
public static function render_recent_post( $args ) {
$recent_posts = \wp_get_recent_posts(
[
'numberposts' => intval( $args['postsNumber'] ?? 5 ),
]
);

ob_start();

echo '<ul>';

foreach ( $recent_posts as $post ) {
echo '<li><a href="' . esc_url( $post['guid'] ) . '">' . esc_html( $post['post_title'] ) . '</a></li>';
}

echo '</ul>';

wp_reset_postdata();

return ob_get_clean();
}
}

// Register module.
add_action(
'divi_module_library_modules_dependency_tree',
function( $dependency_tree ) {
$dependency_tree->add_dependency( new D5TutorialSimpleQuickModule() );
}
);

Set default value on module.json

Let's set default value for recent posts element on module.json so that the said default value can be used on both Visual Builder and Frontend. Here's how it looks:

visual-builder/src/module.json
{
"name": "d5-tut/simple-quick-module",
"d4Shortcode": "d5_tut_simple_quick_module",
"title": "Simple Quick Module",
"titles": "Simple Quick Modules",
"category": "module",
"attributes": {
"module": {
"type": "object",
"selector": "{{selector}}",
"default": {
"meta": {
"adminLabel": {
"desktop": {
"value": "Simple Quick Module"
}
}
}
}
},
"title": {
"type": "object",
"selector": "{{selector}} .d5_tut_simple_quick_module_title",
"attributes": {
"class": "d5_tut_simple_quick_module_title"
},
"tagName": "h2",
"inlineEditor": "plainText",
"elementType": "heading",
"childrenSanitizer": "et_core_esc_previously"
},
"content": {
"type": "object",
"selector": "{{selector}} .d5_tut_simple_quick_module_content",
"attributes": {
"class": "d5_tut_simple_quick_module_content"
},
"tagName": "div",
"inlineEditor": "richText",
"childrenSanitizer": "et_core_esc_previously",
"allowHtml": true
},
"recentPosts": {
"type": "object",
"selector": "{{selector}} .d5-tut-simple-quick-module-recent-posts",
"default": {
"innerContent": {
"desktop": {
"value": {
"postsNumber": "3"
}
}
}
}
}
}
}

At this point, you might have question on why default value for recentPost has to be nested object? We'll explain this on upcoming tutorial for deeper explanation. For the time being, let's roll with this.

Update webpack configuration

To make sure that the changes are reflected on Visual Builder, we need to update webpack configuration. We need to add react and react-dom as externals so that webpack doesn't bundle them. This is because Divi Visual Builder already enqueued them and available in global scope.

visual-builder/webpack.config.js
const path = require('path');

module.exports = {
// Webpack starts bundling the assets from the following file.
// @see https://webpack.js.org/concepts/#entry
entry: {
bundle: './src/index.jsx',
},

// Divi Visual Builder use of scripts that is already enqueued by WordPress and available
// in global scope so those scripts don't need to be included on the bundle. For webpack
// to recognize those files, the global variable needs to be registered as externals.
// These allows global variable listed below to be imported into the module.
// @see https://webpack.js.org/configuration/externals/#externals
externals: {
react: ['vendor', 'React'],
'react-dom': ['vendor', 'ReactDOM'],
},

// This option determine how different types of module within the project will be treated.
// @see https://webpack.js.org/configuration/module/
module: {

// This option sets up loaders for webpack configuration.
// Loaders allow webpack to process various types because by default webpack only
// understand JavaScript and JSON files.
// @see https://webpack.js.org/concepts/#loaders
rules: [
// Handle `.jsx` files.
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [

// Spawns multiple processes and split work between them. This makes faster build.
// @see https://webpack.js.org/loaders/thread-loader/
{
loader: 'thread-loader',
options: {
workers: - 1,
},
},

// Transpiles JavaScript files using Babel. Translates newer syntax with less support
// into older syntax with more support so the project can use newer syntax and have
// them automatically translated into older syntax for compatibility suppoert.
// @see https://www.npmjs.com/package/babel-loader
// @see https://babeljs.io/
{
loader: 'babel-loader',
options: {
compact: false,
presets: [

// Preset that adds configuration for handling latest JavaScript syntax.
// @see https://babeljs.io/docs/en/babel-preset-env
['@babel/preset-env', {
modules: false,
targets: '> 5%',
}],

// Preset that added configuration for handling react & JSX.
// @see https://babeljs.io/docs/en/babel-preset-react
'@babel/preset-react',
],
cacheDirectory: false,
},
}
]
},
]
},

// Determine how modules are resolved.
// @see https://webpack.js.org/configuration/resolve/
resolve: {
// Allows extension to be leave off when importing.
// @see https://webpack.js.org/configuration/resolve/#resolveextensions
extensions: ['.js', '.jsx'],
},

// Determine where the created bundles will be outputted.
// @see https://webpack.js.org/concepts/#output
output: {
filename: 'd5-tutorial-simple-quick-module.js',
path: path.resolve(__dirname, 'build'),
},
};

Render recent post on Visual Builder

Since both module renderer and its module settings are still on the same page, we have to do this at once:

  1. Fetch the unordered list of recent post via REST API using useFetch then render it
  2. Add field on module settings to control the number of posts to be displayed on the unordered list
visual-builder/src/index.jsx
// External library dependencies.
import React, { useEffect, useRef } from 'react';

// WordPress package dependencies.
const { addAction } = window?.vendor?.wp?.hooks;

// Divi package dependencies.
const {
RichTextContainer,
TextContainer,
RangeContainer,
} = window?.divi?.fieldLibrary;
const {
// Renderer - HTML
ModuleContainer,

// Renderer - Styles
StyleContainer,

// Renderer - Classnames
elementClassnames,

// Settings - Content
GroupContainer,
AdminLabelGroup,
BackgroundGroup,
FieldContainer,

// Settings - Design
AnimationGroup,
BorderGroup,
BoxShadowGroup,
FiltersGroup,
FontGroup,
FontBodyGroup,
SizingGroup,
SpacingGroup,
TransformGroup,

// Settings - Advanced
PositionSettingsGroup,
ScrollSettingsGroup,
TransitionGroup,
VisibilitySettingsGroup,
} = window?.divi?.module;
const { registerModule } = window?.divi?.moduleLibrary;
const { useFetch } = window?.divi?.rest;

// Module metadata that is used in both Frontend and Visual Builder.
import metadata from './module.json';

/**
* React function component for rendering module style.
*/
const ModuleStyles = ({
attrs,
elements,
settings,
orderClass,
mode,
state,
noStyleTag
}) => (
<StyleContainer mode={mode} state={state} noStyleTag={noStyleTag}>
{/* Element: Module */}
{elements.style({
attrName: 'module',
styleProps: {
disabledOn: {
disabledModuleVisibility: settings?.disabledModuleVisibility
}
}
})}

{/* Element: Title */}
{elements.style({
attrName: 'title',
})}

{/* Element: Content */}
{elements.style({
attrName: 'content',
})}
</StyleContainer>
);

/**
* React function component for registering module script data.
*/
const ModuleScriptData = ({
elements,
}) => (
<React.Fragment>
{elements.scriptData({
attrName: 'module',
})}
</React.Fragment>
);

/**
* Function for registering module classnames.
*/
const moduleClassnames = ({
classnamesInstance,
attrs,
}) => {
// Add element classnames.
classnamesInstance.add(
elementClassnames({
attrs: attrs?.module?.decoration ?? {},
})
);
};

/**
* Simple Quick Module.
*/
const simpleQuickModule = {
// Metadata that is used on Visual Builder and Frontend
metadata,
// Layout renderer components.
renderers: {
// React Function Component for rendering module's output on layout area.
edit: ({ attrs, id, name, elements }) => {
// Divi's hook for fetching value from REST API, and handling loading state.
const { fetch, response, isLoading } = useFetch([]);

// Reference for handling fetch abort.
const fetchAbortRef = useRef();

// Attribute for handling number of posts to be displayed.
// If you have question on why it has to be in nested object, we'll explain this on the later tutorial.
const postsNumber = attrs?.recentPosts?.innerContent?.desktop?.value?.postsNumber;

// React hook that will execute the callback (in this case, fetching value from REST API endpoint)
// whenever the `postsNumber` value is changed.
useEffect(() => {
// Abort previous fetch if there's any.
if (fetchAbortRef.current) {
fetchAbortRef.current.abort();
}

// Create new AbortController instance.
fetchAbortRef.current = new AbortController();

// Fetch value from REST API endpoint. The returned value will be available
// on `response` properties of `useFetch` hook declaration above.
fetch({
method: 'GET',
restRoute: `/wp/v2/posts?context=view&per_page=${postsNumber}`,
signal: fetchAbortRef.current.signal,
}).catch((error) => {
console.log(error);
});

return () => {
if (fetchAbortRef.current) {
fetchAbortRef.current.abort();
}
};

}, [postsNumber]);

return (
<ModuleContainer
attrs={attrs}
elements={elements}
id={id}
moduleClassName="d5_tut_simple_quick_module"
name={name}
scriptDataComponent={ModuleScriptData}
stylesComponent={ModuleStyles}
classnamesFunction={moduleClassnames}
>
{elements.styleComponents({ attrName: 'module' })}
<div className="et_pb_module_inner">
{elements.render({ attrName: 'title' })}
{elements.render({ attrName: 'content' })}
{isLoading ? 'Loading...' : (
<div className="d5-tut-simple-quick-module-recent-posts">
<ul>
{response.map((post) => (
<li key={post.id} className="post-item">
<a href={post.link}>{post.title.rendered}</a>
</li>
))}
</ul>
</div>
)}
</div>
</ModuleContainer>
);
},
},
// Settings component.
settings: {
// React function component that renders module settings' content panel.
content: ({ defaultSettingsAttrs }) => (
<React.Fragment>
<GroupContainer id="mainContent" title="Text">
<FieldContainer attrName="title.innerContent" label="Title" description="Title">
<TextContainer />
</FieldContainer>
<FieldContainer attrName="content.innerContent" label="Content">
<RichTextContainer />
</FieldContainer>
{/* New field for modifying number of posts that will be rendered */}
<FieldContainer attrName="recentPosts.innerContent" subName="postsNumber" label="Number of Posts">
<RangeContainer min={1} minLimit={1} max={10} maxLimit={10} defaultUnit="" allowedUnits={['']} />
</FieldContainer>
</GroupContainer>
<BackgroundGroup />
<AdminLabelGroup defaultGroupAttr={defaultSettingsAttrs?.adminLabel} />
</React.Fragment>
),
// React function component that renders module settings' design panel.
design: () => (
<React.Fragment>
<FontGroup attrName="title.decoration.font" groupLabel="Title Font" />
<FontBodyGroup attrName="content.decoration.bodyFont" groupLabel="Content Font" />
<SizingGroup />
<SpacingGroup />
<BorderGroup />
<BoxShadowGroup />
<FiltersGroup />
<TransformGroup />
<AnimationGroup />
</React.Fragment>
),
// React function component that renders module settings' advanced panel.
advanced: () => (
<React.Fragment>
<VisibilitySettingsGroup />
<TransitionGroup />
<PositionSettingsGroup />
<ScrollSettingsGroup />
</React.Fragment>
),
},
// Attribute that is automatically added into the module when the module is inserted
// into the layout so that the newly inserted module has some placeholder content
placeholderContent: {
module: {
decoration: {
background: {
desktop: {
value: {
color: '#DFDFDF',
},
},
},
},
},
title: {
innerContent: {
desktop: {
value: 'Module Title',
},
},
},
content: {
innerContent: {
desktop: {
value: 'Module Content',
},
},
},
},
};

// Register module.
addAction('divi.moduleLibrary.registerModuleLibraryStore.after', 'd5Tut.simpleQuickModule', () => {
registerModule(simpleQuickModule.metadata, simpleQuickModule);
});

Generate build files

Now that all necessary changes have been done, you can generate new build file. Open command line / terminal, go to visual-builder directory, then run:

Windows Users

If you're on Windows, you may encounter an error: 'NODE_ENV' is not recognized as an internal or external command. To fix this, install the win-node-env package:

npm install --save-optional win-node-env

Then run the build command again. This package enables Windows to recognize the NODE_ENV environment variable used in the npm scripts.

npm run build

Now you can see how it works by adding "Simple Quick Module" via visual builder.

Got a question or feedback? Ask away!