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
initfunction infrontend.jsto 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
localStoragefor 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_scriptcall 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
updatePricesfunction 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.