Building Reusable Components in Gutenberg with Vue: A Comprehensive Guide

Gutenberg, the WordPress block editor, offers a powerful platform for building dynamic and engaging website content. While WordPress offers a wide range of built-in blocks, sometimes you need to create custom blocks to meet specific needs. This is where the integration of Vue.js comes into play, allowing you to build highly reusable and interactive components within Gutenberg.

This comprehensive guide will take you through the process of building reusable Gutenberg blocks with Vue.js, covering everything from setup to deployment.

1. Setting the Stage: Project Setup and Dependencies

Before diving into code, we need to set up our project environment.

1.1. Project Setup:

First, create a new WordPress plugin directory within your /wp-content/plugins folder. For this guide, we’ll name it "gutenberg-vue-components." Inside, create a src directory and two files:

  • index.js: This is the main file that will be loaded by WordPress.
  • gutenberg-vue-block.js: This will contain our Vue component code.

1.2. Essential Dependencies:

Install the following packages:

npm install vue vue-loader @wordpress/scripts @wordpress/block-editor @wordpress/components @wordpress/data

These packages are vital for:

  • vue: The core Vue.js library.
  • vue-loader: Allows you to import and process Vue components within Webpack.
  • @wordpress/scripts: Provides Webpack configuration and tools for building Gutenberg blocks.
  • @wordpress/block-editor: Offers Gutenberg-specific APIs and components.
  • @wordpress/components: Provides pre-built components like buttons, inputs, and more.
  • @wordpress/data: Provides a mechanism for accessing and managing WordPress data.

2. Crafting Our First Reusable Component:

Let’s create a simple "Quote" block using Vue.

2.1. Defining the Vue Component:

In gutenberg-vue-block.js, create a Vue component:

import { registerBlockType } from '@wordpress/blocks';
import { 
    InspectorControls,
    RichText,
    useBlockProps,
    BlockControls,
    ToolbarButton 
} from '@wordpress/block-editor';
import { PanelBody, TextControl, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n'; 

const QuoteBlock = () => {
    const blockProps = useBlockProps();
    const [quoteText, setQuoteText] = useState('');
    const [author, setAuthor] = useState('');
    const [showAuthor, setShowAuthor] = useState(true);

    const handleQuoteChange = (newQuote) => {
        setQuoteText(newQuote);
    };

    const handleAuthorChange = (newAuthor) => {
        setAuthor(newAuthor);
    };

    const toggleAuthor = () => {
        setShowAuthor(!showAuthor);
    };

    return (
        <div {...blockProps}>
            <BlockControls>
                <ToolbarButton
                    icon="format-quote"
                    label={__("Quote Settings")}
                    onClick={toggleAuthor}
                />
            </BlockControls>
            <InspectorControls>
                <PanelBody title={__("Quote Settings")}>
                    <TextControl
                        label={__("Quote Text")}
                        value={quoteText}
                        onChange={handleQuoteChange}
                    />
                    <TextControl
                        label={__("Author")}
                        value={author}
                        onChange={handleAuthorChange}
                    />
                    <ToggleControl
                        label={__("Show Author")}
                        checked={showAuthor}
                        onChange={toggleAuthor}
                    />
                </PanelBody>
            </InspectorControls>
            <div className="quote-container">
                <RichText
                    tagName="blockquote"
                    value={quoteText}
                    onChange={handleQuoteChange}
                />
                {showAuthor && <footer>
                    <cite>{author}</cite>
                </footer>}
            </div>
        </div>
    );
};

registerBlockType('my-plugin/quote-block', {
    title: __('Quote Block'),
    icon: 'format-quote',
    category: 'common',
    edit: QuoteBlock,
    save: (props) => {
        const { attributes } = props;
        return (
            <blockquote className="wp-block-quote">
                <p>{attributes.quoteText}</p>
                {attributes.showAuthor && 
                    <footer>
                        <cite>{attributes.author}</cite>
                    </footer>
                }
            </blockquote>
        );
    },
});

Explanation:

  • We use registerBlockType from @wordpress/blocks to register our block.
  • The edit prop defines our Vue component as the block’s edit view.
  • The save prop defines the block’s output when saved.
  • useBlockProps provides props for styling and attributes.
  • InspectorControls allows adding settings within the sidebar.
  • PanelBody groups settings into a panel.
  • TextControl creates input fields for text.
  • ToggleControl creates a switch for toggling options.
  • RichText provides a WYSIWYG editor for the quote text.
  • BlockControls adds controls to the block toolbar.
  • We use state management for the quote text, author, and show author options.

2.2. Building the Block’s Frontend (Save Function):

The save function determines how the block will be rendered on the frontend. In this example, we create a standard HTML blockquote structure.

2.3. Loading the Script:

In your index.js, import the Vue component and initialize it using Webpack:

import './style.css';
import './gutenberg-vue-block.js';

const { registerPlugin } = wp.plugins;
const { __ } = wp.i18n;

registerPlugin('my-plugin/gutenberg-vue-components', {
    render: () => {
        return null;
    },
});

3. Expanding Functionality: Building More Complex Components

Now, let’s create a reusable "Call to Action" block.

3.1. The "Call to Action" Component:

In gutenberg-vue-block.js, create a new Vue component:

import { registerBlockType } from '@wordpress/blocks';
import {
    InspectorControls,
    RichText,
    useBlockProps,
    BlockControls,
    ToolbarButton,
    AlignmentToolbar
} from '@wordpress/block-editor';
import { PanelBody, TextControl, SelectControl, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

const CallToActionBlock = () => {
    const blockProps = useBlockProps();
    const [title, setTitle] = useState('');
    const [buttonText, setButtonText] = useState('');
    const [linkUrl, setLinkUrl] = useState('');
    const [alignment, setAlignment] = useState('center');

    const handleTitleChange = (newTitle) => {
        setTitle(newTitle);
    };

    const handleButtonTextChange = (newButtonText) => {
        setButtonText(newButtonText);
    };

    const handleLinkUrlChange = (newLinkUrl) => {
        setLinkUrl(newLinkUrl);
    };

    const handleAlignmentChange = (newAlignment) => {
        setAlignment(newAlignment);
    };

    return (
        <div {...blockProps}>
            <BlockControls>
                <ToolbarButton
                    icon="align-left"
                    label={__("Align Left")}
                    onClick={() => handleAlignmentChange('left')}
                    isActive={alignment === 'left'}
                />
                <ToolbarButton
                    icon="align-center"
                    label={__("Align Center")}
                    onClick={() => handleAlignmentChange('center')}
                    isActive={alignment === 'center'}
                />
                <ToolbarButton
                    icon="align-right"
                    label={__("Align Right")}
                    onClick={() => handleAlignmentChange('right')}
                    isActive={alignment === 'right'}
                />
            </BlockControls>
            <InspectorControls>
                <PanelBody title={__("Call to Action Settings")}>
                    <TextControl
                        label={__("Title")}
                        value={title}
                        onChange={handleTitleChange}
                    />
                    <TextControl
                        label={__("Button Text")}
                        value={buttonText}
                        onChange={handleButtonTextChange}
                    />
                    <TextControl
                        label={__("Link URL")}
                        value={linkUrl}
                        onChange={handleLinkUrlChange}
                    />
                    <SelectControl
                        label={__("Alignment")}
                        value={alignment}
                        onChange={handleAlignmentChange}
                        options={[
                            { label: __("Left"), value: 'left' },
                            { label: __("Center"), value: 'center' },
                            { label: __("Right"), value: 'right' },
                        ]}
                    />
                </PanelBody>
            </InspectorControls>
            <div className={`cta-container ${alignment}`}>
                {title && <h2 className="cta-title">{title}</h2>}
                <a href={linkUrl} className="cta-button">{buttonText}</a>
            </div>
        </div>
    );
};

registerBlockType('my-plugin/call-to-action-block', {
    title: __('Call to Action Block'),
    icon: 'admin-post',
    category: 'common',
    edit: CallToActionBlock,
    save: (props) => {
        const { attributes } = props;
        return (
            <div className={`cta-container ${attributes.alignment}`}>
                {attributes.title && <h2 className="cta-title">{attributes.title}</h2>}
                <a href={attributes.linkUrl} className="cta-button">{attributes.buttonText}</a>
            </div>
        );
    },
});

3.2. Building the Block’s Frontend (Save Function):

The save function constructs the frontend HTML for the call-to-action block based on the attributes. We use classes to apply styling for alignment.

4. Adding Dynamic Behavior: Interacting with WordPress Data

Let’s create a block that dynamically displays a list of recent posts using Vue.js and the WordPress data API.

4.1. The "Recent Posts" Component:

import { registerBlockType } from '@wordpress/blocks';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';

const RecentPostsBlock = () => {
    const blockProps = useBlockProps();
    const [postCount, setPostCount] = useState(3);

    const handlePostCountChange = (newPostCount) => {
        setPostCount(newPostCount);
    };

    const { getPosts } = useSelect((select) => ({
        getPosts: select('core').getPosts,
    }), []);

    const posts = getPosts({
        per_page: postCount,
    });

    return (
        <div {...blockProps}>
            <InspectorControls>
                <PanelBody title={__("Recent Posts Settings")}>
                    <TextControl
                        label={__("Number of Posts")}
                        value={postCount}
                        onChange={handlePostCountChange}
                        type="number"
                    />
                </PanelBody>
            </InspectorControls>
            <ul>
                {posts.map((post) => (
                    <li key={post.id}>
                        <a href={post.link}>
                            {post.title.rendered}
                        </a>
                    </li>
                ))}
            </ul>
        </div>
    );
};

registerBlockType('my-plugin/recent-posts-block', {
    title: __('Recent Posts Block'),
    icon: 'post-formats',
    category: 'common',
    edit: RecentPostsBlock,
    save: (props) => {
        const { attributes } = props;
        return (
            <ul>
                {attributes.postCount > 0 && 
                    <p>{__("This block will display recent posts on the frontend.")}</p>
                }
            </ul>
        );
    },
});

4.2. Explanation:

  • We use useSelect to access the core store and get the list of posts.
  • The getPosts function fetches the recent posts with the specified per_page count.
  • The posts array is then mapped to display each post title in a list.
  • The save function includes placeholder text for the frontend, as the recent posts are dynamically loaded on the frontend.

5. Integrating with External APIs and Custom Data

We can go beyond WordPress data and fetch information from external APIs or custom data sources using the fetch API or libraries like axios.

5.1. The "Weather" Component (using OpenWeatherMap API):

import { registerBlockType } from '@wordpress/blocks';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from 'react';

const WeatherBlock = () => {
    const blockProps = useBlockProps();
    const [city, setCity] = useState('');
    const [weatherData, setWeatherData] = useState(null);

    const handleCityChange = (newCity) => {
        setCity(newCity);
    };

    useEffect(() => {
        const apiKey = 'YOUR_API_KEY'; // Replace with your OpenWeatherMap API key
        const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;

        if (city) {
            fetch(apiUrl)
                .then(response => response.json())
                .then(data => setWeatherData(data))
                .catch(error => console.error('Error fetching weather data:', error));
        }
    }, [city]);

    return (
        <div {...blockProps}>
            <InspectorControls>
                <PanelBody title={__("Weather Settings")}>
                    <TextControl
                        label={__("City")}
                        value={city}
                        onChange={handleCityChange}
                    />
                </PanelBody>
            </InspectorControls>
            {weatherData && (
                <div>
                    <h2>{weatherData.name}</h2>
                    <p>Temperature: {weatherData.main.temp}°C</p>
                    <p>Description: {weatherData.weather[0].description}</p>
                </div>
            )}
        </div>
    );
};

registerBlockType('my-plugin/weather-block', {
    title: __('Weather Block'),
    icon: 'cloud',
    category: 'common',
    edit: WeatherBlock,
    save: (props) => {
        const { attributes } = props;
        return (
            <div>
                {attributes.city && <p>{__("This block will display weather data on the frontend.")}</p>}
            </div>
        );
    },
});

5.2. Explanation:

  • The WeatherBlock component fetches weather data from the OpenWeatherMap API using the useEffect hook.
  • The city state is used to store the city name entered by the user.
  • The weatherData state stores the fetched weather data.
  • The save function includes placeholder text for the frontend, as the weather data is dynamically loaded on the frontend.

6. Deploying Your Gutenberg Blocks

To deploy your Gutenberg blocks, follow these steps:

6.1. Build the Plugin:

Run npm run build in the plugin directory to generate the production-ready code. This creates a build folder containing the compiled files.

6.2. Activate the Plugin:

Go to the Plugins section in your WordPress dashboard and activate the "gutenberg-vue-components" plugin.

6.3. Use Your Blocks:

You can now use your reusable blocks in any post or page within your WordPress editor.

7. Best Practices and Tips

  • Use CSS Modules or SCSS: For consistent styling, use CSS Modules or SCSS for component-specific styles.
  • Component Reusability: Design your components to be as reusable as possible, keeping them modular and self-contained.
  • State Management: Consider using a state management solution like Vuex for more complex applications.
  • Testing: Write unit tests for your components to ensure they function correctly.
  • Accessibility: Follow accessibility guidelines to ensure your blocks are usable by everyone.

Conclusion

By integrating Vue.js with Gutenberg, you can unlock a new level of interactivity and functionality within the WordPress block editor. This guide has provided a foundation for building reusable components, fetching data dynamically, and creating engaging user experiences. With these tools and best practices, you can build custom Gutenberg blocks that are both powerful and aesthetically pleasing.

Remember, this is just a starting point. Explore the vast potential of Vue.js and Gutenberg to create truly unique and engaging website content.

Leave a Reply

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

Trending