• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom multi-currency switcher block for Gutenberg using Alpine.js lightweight states

Step-by-Step Guide to building a custom multi-currency switcher block for Gutenberg using Alpine.js lightweight states

Gutenberg Block Structure and Registration

We’ll begin by defining the core structure of our Gutenberg block. This involves creating a JavaScript file that registers the block with WordPress and defines its editable attributes. For a multi-currency switcher, we’ll need attributes to store the available currencies, their symbols, and potentially default exchange rates or API endpoints.

Create a new directory for your plugin, e.g., wp-content/plugins/custom-currency-switcher. Inside this directory, create a main plugin file (e.g., custom-currency-switcher.php) and a JavaScript directory (e.g., src/js).

Plugin PHP File (custom-currency-switcher.php)

This file handles plugin activation, enqueuing our JavaScript, and registering the Gutenberg block.

<?php
/**
 * Plugin Name: Custom Currency Switcher Block
 * Description: A Gutenberg block for a custom multi-currency switcher.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: custom-currency-switcher
 */

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

/**
 * Register the Gutenberg block.
 */
function ccs_register_block() {
    // Automatically load dependencies and version.
    $asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');

    wp_register_script(
        'custom-currency-switcher-block-editor-script',
        plugins_url( 'build/index.js', __FILE__ ),
        $asset_file['dependencies'],
        $asset_file['version']
    );

    wp_register_style(
        'custom-currency-switcher-block-editor-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array( 'wp-edit-blocks' ),
        $asset_file['version']
    );

    register_block_type( 'custom-currency-switcher/block', array(
        'editor_script' => 'custom-currency-switcher-block-editor-script',
        'editor_style'  => 'custom-currency-switcher-block-editor-style',
        'render_callback' => 'ccs_render_currency_switcher_block',
    ) );
}
add_action( 'init', 'ccs_register_block' );

/**
 * Server-side rendering for the currency switcher block.
 *
 * @param array $attributes Block attributes.
 * @return string HTML output.
 */
function ccs_render_currency_switcher_block( $attributes ) {
    // This function will be implemented later for front-end rendering.
    // For now, we'll rely on JavaScript for dynamic updates.
    return '<div id="custom-currency-switcher-frontend"></div>';
}
?>

JavaScript Block Registration (src/js/index.js)

This file uses the Gutenberg Block API to define our block’s attributes and its editor interface. We’ll use `wp.blocks.registerBlockType` for this.

const { registerBlockType } = wp.blocks;
const { RichText, InspectorControls } = wp.editor;
const { PanelBody, SelectControl, TextControl, Button } = wp.components;
const { useState, useEffect } = wp.element;

registerBlockType( 'custom-currency-switcher/block', {
    title: 'Custom Currency Switcher',
    icon: 'money',
    category: 'ecommerce', // Or 'widgets', 'design', etc.
    attributes: {
        currencies: {
            type: 'array',
            default: [
                { code: 'USD', symbol: '$', rate: 1.0 },
                { code: 'EUR', symbol: '€', rate: 0.92 },
                { code: 'GBP', symbol: '£', rate: 0.79 },
            ],
        },
        defaultCurrency: {
            type: 'string',
            default: 'USD',
        },
    },

    edit: ( { attributes, setAttributes } ) => {
        const { currencies, defaultCurrency } = attributes;

        const handleCurrencyChange = ( index, field, value ) => {
            const newCurrencies = [...currencies];
            newCurrencies[index][field] = value;
            setAttributes( { currencies: newCurrencies } );
        };

        const addCurrency = () => {
            setAttributes( {
                currencies: [
                    ...currencies,
                    { code: '', symbol: '', rate: 1.0 },
                ],
            } );
        };

        const removeCurrency = ( index ) => {
            const newCurrencies = currencies.filter( ( _, i ) => i !== index );
            setAttributes( { currencies: newCurrencies } );
        };

        const handleDefaultCurrencyChange = ( value ) => {
            setAttributes( { defaultCurrency: value } );
        };

        // Populate select control options dynamically
        const currencyOptions = currencies.map( ( currency, index ) => ( {
            label: `${ currency.code } (${ currency.symbol })`,
            value: currency.code,
        } ) );

        return (
            <>
                <InspectorControls>
                    <PanelBody title="Currency Settings" initialOpen={ true }>
                        { currencies.map( ( currency, index ) => (
                            <div key={ index } style={ { marginBottom: '15px', border: '1px solid #ccc', padding: '10px' } }>
                                <TextControl
                                    label="Currency Code (e.g., USD)"
                                    value={ currency.code }
                                    onChange={ ( value ) => handleCurrencyChange( index, 'code', value ) }
                                />
                                <TextControl
                                    label="Currency Symbol (e.g., $)"
                                    value={ currency.symbol }
                                    onChange={ ( value ) => handleCurrencyChange( index, 'symbol', value ) }
                                />
                                <TextControl
                                    label="Exchange Rate (to default)"
                                    type="number"
                                    step="0.001"
                                    value={ currency.rate }
                                    onChange={ ( value ) => handleCurrencyChange( index, 'rate', parseFloat( value ) ) }
                                />
                                <Button isDestructive onClick={ () => removeCurrency( index ) }>
                                    Remove Currency
                                </Button>
                            </div>
                        ) ) }
                        <Button isPrimary onClick={ addCurrency }>
                            Add Currency
                        </Button>
                        <hr />
                        <SelectControl
                            label="Default Currency"
                            value={ defaultCurrency }
                            options={ currencyOptions }
                            onChange={ handleDefaultCurrencyChange }
                        />
                    </PanelBody>
                </InspectorControls>
                <div className="custom-currency-switcher-editor">
                    <h4>Currency Switcher Preview</h4>
                    <p>Default: { defaultCurrency }</p>
                    <ul>
                        { currencies.map( ( currency, index ) => (
                            <li key={ index }>{ currency.code } ({ currency.symbol })</li>
                        ) ) }
                    </ul>
                </div>
            </>
        );
    },

    save: () => {
        // The actual rendering will be handled by the PHP render_callback
        // or a client-side script. For simplicity, we return null here
        // as the render_callback will output the necessary container.
        return null;
    },
} );

Build Process and Asset Compilation

To compile our JavaScript and CSS, we’ll use `@wordpress/scripts`. This package provides a convenient build process that handles transpilation, bundling, and minification.

Setting up the Build Environment

Navigate to your plugin’s root directory in your terminal and run the following commands to initialize your Node.js project and install the necessary dependencies:

cd wp-content/plugins/custom-currency-switcher
npm init -y
npm install @wordpress/scripts --save-dev

Next, update your package.json file to include build scripts. Add the following lines to the scripts section:

{
  "name": "custom-currency-switcher",
  "version": "1.0.0",
  "description": "A Gutenberg block for a custom multi-currency switcher.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": ["wordpress", "gutenberg", "block", "ecommerce", "currency"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "devDependencies": {
    "@wordpress/scripts": "^26.7.0"
  }
}

Now, you can run the build commands:

npm run build

This command will create a build directory containing index.js (your compiled JavaScript) and index.asset.php (which lists dependencies and version for WordPress to use). If you want to watch for changes during development, use npm run start.

Frontend Implementation with Alpine.js

For the frontend, we’ll leverage Alpine.js for its lightweight reactivity and state management. This avoids the need for a full JavaScript framework on the client-side, keeping the page load fast.

Enqueuing Alpine.js and Frontend Script

We need to enqueue Alpine.js and our custom frontend JavaScript file. We’ll do this in the main plugin PHP file.

 ccs_get_all_currency_switcher_blocks_attributes(),
    ) );
}
add_action( 'wp_enqueue_scripts', 'ccs_enqueue_frontend_scripts' );

/**
 * Helper function to get attributes of all currency switcher blocks on the page.
 * This is a simplified approach; a more robust solution might involve
 * fetching block data from post content.
 *
 * @return array
 */
function ccs_get_all_currency_switcher_blocks_attributes() {
    // In a real-world scenario, you'd parse the post content to find block attributes.
    // For this example, we'll assume attributes are passed via a shortcode or
    // a more direct method if blocks are rendered server-side.
    // For now, let's return a placeholder or fetch from a transient/option if set.
    // A more advanced approach would involve using `parse_blocks()` on `get_the_content()`.

    // Example using parse_blocks (requires context, e.g., within the loop)
    if ( is_main_query() && in_the_loop() ) {
        $blocks = parse_blocks( get_the_content() );
        $currency_blocks = array();
        foreach ( $blocks as $block ) {
            if ( $block['blockName'] === 'custom-currency-switcher/block' ) {
                $currency_blocks[] = $block['attrs'];
            }
        }
        return $currency_blocks;
    }
    return array(); // Return empty if not in loop or main query
}

// Update the render_callback to output a container with Alpine.js directives
function ccs_render_currency_switcher_block( $attributes ) {
    // We need a unique ID for each instance if multiple blocks are on a page.
    static $instance_count = 0;
    $instance_count++;
    $block_id = 'ccs-block-' . $instance_count;

    // Pass attributes directly to the container for Alpine.js to pick up.
    // JSON.stringify is crucial here.
    $data_attributes = '';
    if ( ! empty( $attributes ) ) {
        $data_attributes = 'data-ccs-attributes="' . esc_attr( json_encode( $attributes ) ) . '"';
    }

    return sprintf(
        '
', esc_attr( $block_id ), $data_attributes ); } ?>

Frontend JavaScript (src/js/frontend.js)

This script will initialize Alpine.js components on the frontend, allowing users to switch currencies. It will read the block’s attributes passed from PHP.

document.addEventListener('alpine:init', () => {
    // Find all instances of our currency switcher block on the page.
    const switcherContainers = document.querySelectorAll('.custom-currency-switcher-frontend');

    switcherContainers.forEach(container => {
        // Retrieve attributes passed via data attribute.
        const attributesJson = container.getAttribute('data-ccs-attributes');
        let blockAttributes = {
            currencies: [],
            defaultCurrency: 'USD',
        };

        if (attributesJson) {
            try {
                blockAttributes = JSON.parse(attributesJson);
            } catch (e) {
                console.error('Error parsing currency switcher attributes:', e);
            }
        }

        // Initialize Alpine.js component for this specific container.
        Alpine.data(`currencySwitcher_${container.id}`, () => ({
            // State from block attributes
            currencies: blockAttributes.currencies || [],
            defaultCurrency: blockAttributes.defaultCurrency || 'USD',

            // Local state for the switcher
            currentCurrency: blockAttributes.defaultCurrency || 'USD',
            currencyRates: {}, // To store fetched or static rates

            init() {
                // Set initial currency rates from attributes
                this.currencies.forEach(currency => {
                    this.currencyRates[currency.code] = currency.rate;
                });

                // Load saved currency from localStorage if available
                const savedCurrency = localStorage.getItem(`ccs_currency_${container.id}`);
                if (savedCurrency && this.currencies.some(c => c.code === savedCurrency)) {
                    this.currentCurrency = savedCurrency;
                } else {
                    // Ensure default currency is valid
                    if (!this.currencies.some(c => c.code === this.defaultCurrency)) {
                        this.currentCurrency = this.currencies.length > 0 ? this.currencies[0].code : 'USD';
                    } else {
                        this.currentCurrency = this.defaultCurrency;
                    }
                }

                // Trigger initial price update if prices exist on the page
                this.updatePrices();
            },

            // Method to switch currency
            switchCurrency(currencyCode) {
                if (this.currencies.some(c => c.code === currencyCode)) {
                    this.currentCurrency = currencyCode;
                    localStorage.setItem(`ccs_currency_${container.id}`, currencyCode);
                    this.updatePrices();
                }
            },

            // Method to get the symbol for the current currency
            getCurrencySymbol() {
                const currency = this.currencies.find(c => c.code === this.currentCurrency);
                return currency ? currency.symbol : '';
            },

            // Method to get the rate for the current currency relative to the default
            getCurrentRate() {
                return this.currencyRates[this.currentCurrency] || 1.0;
            },

            // Method to apply currency conversion to prices on the page
            updatePrices() {
                const currentRate = this.getCurrentRate();
                const defaultCurrencySymbol = this.currencies.find(c => c.code === this.defaultCurrency)?.symbol || '$';

                // Example: Target elements with a specific class and data attribute for original price
                // This part is highly dependent on your e-commerce theme/plugin structure.
                // You'll need to adapt selectors to match your site's product prices.
                document.querySelectorAll('.product-price, .woocommerce-Price-amount').forEach(priceElement => {
                    const originalPriceAttr = priceElement.getAttribute('data-original-price');
                    if (originalPriceAttr) {
                        const originalPrice = parseFloat(originalPriceAttr);
                        if (!isNaN(originalPrice)) {
                            const convertedPrice = originalPrice * currentRate;
                            // Format the price (basic example, consider using Intl.NumberFormat for better localization)
                            priceElement.textContent = `${this.getCurrencySymbol()}${convertedPrice.toFixed(2)}`;
                        }
                    } else {
                        // If data-original-price is not set, try to parse the current text content.
                        // This is less reliable as it depends on the existing format.
                        const priceText = priceElement.textContent.replace(/[^0-9.]/g, '');
                        const originalPrice = parseFloat(priceText);
                        if (!isNaN(originalPrice)) {
                            priceElement.setAttribute('data-original-price', originalPrice); // Store for future conversions
                            const convertedPrice = originalPrice * currentRate;
                            priceElement.textContent = `${this.getCurrencySymbol()}${convertedPrice.toFixed(2)}`;
                        }
                    }
                });
            },

            // Helper to display price in default currency for reference
            displayDefaultPrice(originalPrice) {
                 if (typeof originalPrice === 'number') {
                     return `${defaultCurrencySymbol}${originalPrice.toFixed(2)}`;
                 }
                 return '';
            }
        }));

        // Initialize the component by accessing it.
        // This ensures Alpine.js processes the directives within the container.
        const componentInstance = Alpine.$(`[x-data='currencySwitcher_${container.id}']`);
        if (componentInstance) {
            // Manually trigger init if needed, though Alpine usually handles this.
            // componentInstance.init(); // Alpine.js v3 handles initialization automatically.
        }
    });
});

// Function to ensure prices are correctly set up on page load and after AJAX updates
function setupCurrencySwitcher() {
    // Ensure all price elements have their original price stored.
    document.querySelectorAll('.product-price, .woocommerce-Price-amount').forEach(priceElement => {
        if (!priceElement.hasAttribute('data-original-price')) {
            const priceText = priceElement.textContent.replace(/[^0-9.]/g, '');
            const originalPrice = parseFloat(priceText);
            if (!isNaN(originalPrice)) {
                priceElement.setAttribute('data-original-price', originalPrice);
            }
        }
    });

    // Re-run Alpine.js initialization if necessary, especially after AJAX content loads.
    // Alpine.initTree(); // Use this if you suspect components aren't initializing.
}

// Initial setup
document.addEventListener('DOMContentLoaded', setupCurrencySwitcher);

// Handle potential AJAX updates (e.g., WooCommerce add to cart)
// This is a simplified example; you might need more specific hooks.
document.body.addEventListener('ajaxComplete', setupCurrencySwitcher); // jQuery AJAX complete hook
document.body.addEventListener('updated_wc_div', setupCurrencySwitcher); // WooCommerce specific hook
document.body.addEventListener('wc_fragments_loaded', setupCurrencySwitcher); // WooCommerce fragments

Integrating with Your Theme/E-commerce Platform

The updatePrices function in frontend.js is the most critical part for integration. You’ll need to adapt the selectors (e.g., .product-price, .woocommerce-Price-amount) and the logic for extracting and setting prices to match your specific WordPress theme and e-commerce plugin (like WooCommerce).

For example, if your product prices are wrapped in a span with the class .price and contain the currency symbol, you might modify the selector and parsing logic. Crucially, ensure that the original price is stored using a data attribute (e.g., data-original-price) so it’s not affected by subsequent conversions.

Example Frontend HTML Structure (using Alpine.js directives)

When you add the block to a post or page, the rendered output will look something like this. The Alpine.js directives are key here.

<div id="ccs-block-1" class="custom-currency-switcher-frontend" data-ccs-attributes='{"currencies":[{"code":"USD","symbol":"$","rate":1},{"code":"EUR","symbol":"€","rate":0.92},{"code":"GBP","symbol":"£","rate":0.79}],"defaultCurrency":"USD"}' x-data="currencySwitcher_ccs-block-1">
    <!-- Currency selection dropdown/buttons -->
    <div class="currency-selector">
        <span>Currency: </span>
        <select x-model="currentCurrency" @change="switchCurrency($event.target.value)">
            <template x-for="currency in currencies" :key="currency.code">
                <option :value="currency.code" x-text="`${currency.code} (${currency.symbol})`"></option>
            </template>
        </select>
    </div>

    <!-- Example product price display -->
    <div class="product-listing">
        <h3>Awesome Product</h3>
        <p class="price" data-original-price="19.99">
            <!-- Price will be dynamically updated by Alpine.js -->
            <span x-text="getCurrencySymbol()"></span><span>19.99</span>
        </p>
        <button>Add to Cart</button>
    </div>

    <!-- Display original price for reference (optional) -->
    <p>
        Original Price:
        <span x-text="displayDefaultPrice(currencies.find(c => c.code === defaultCurrency)?.rate * 19.99)"></span>
        (Assuming 19.99 was the base price in default currency)
    </p>
</div>

Advanced Considerations and Enhancements

Dynamic Exchange Rates

Instead of static rates, you can integrate with a currency exchange rate API (e.g., ExchangeRate-API, Open Exchange Rates). This would involve:

  • Adding an API key and endpoint URL as block attributes.
  • Modifying the init function in frontend.js to fetch rates from the API.
  • Implementing error handling and caching for API requests.
  • Consider using WordPress transients to cache API responses server-side to reduce external requests and improve performance.

WooCommerce Integration

For deeper WooCommerce integration:

  • Use WooCommerce hooks (e.g., woocommerce_product_get_price, woocommerce_get_price_html) to modify prices directly on the backend or frontend.
  • Store the selected currency in a cookie or session rather than localStorage for better persistence across different devices/browsers if needed.
  • Consider using WooCommerce’s built-in currency functionalities if available, or ensure your custom solution plays well with them.
  • The wp_localize_script call in PHP can be enhanced to pass more context, like product IDs or specific price data if needed for complex AJAX updates.

Performance Optimization

While Alpine.js is lightweight, consider these points:

  • Only enqueue scripts on pages where the block is actually used. Use conditional enqueuing based on post content or page templates.
  • Optimize the updatePrices function to be as efficient as possible, especially on pages with many price elements. Debouncing or throttling might be necessary if performance issues arise.
  • Lazy-load the Alpine.js CDN script if it’s not critical for initial page render.

Accessibility

Ensure the currency switcher is accessible:

  • Use semantic HTML elements (e.g., <select>, <button>).
  • Provide clear labels for form elements.
  • Ensure keyboard navigation is functional.
  • Test with screen readers.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala