Data Helpers
Data helpers are utility functions that assist with transforming, validating, and managing data in the Phenix Blocks system. These helpers provide standardized methods for common data operations used across multiple blocks.
Overview
When working with block data, you often need to transform data between different formats, validate user input, or perform other data operations. Data helpers provide reusable functions for:
- Transforming data between formats
- Validating user input
- Filtering and searching data
- Converting between data types
- Handling special data formats
Data Transformers
valueHandler
Extracts and normalizes values from different input types, handling various form controls consistently.
/**
* Extracts and normalizes values from different input types
*
* @param {HTMLElement|String} target - The input element or direct value
* @returns {*} - The normalized value (string, boolean, or array)
*/
function valueHandler(target) {
// Define variables
let single_val;
let array_val = [];
let type = target instanceof HTMLElement ? (target.getAttribute('type') || target.tagName) : null;
// Handle boolean values (checkboxes, radios, boolean attributes)
if (target === "boolean" || type === "boolean" || type === 'checkbox' || type === 'radio') {
if (target.value === 'boolean') {
single_val = target.checked;
} else {
single_val = target.checked ? target.value : "";
}
}
// Handle multi-select dropdowns
else if (type === "SELECT" && target.hasAttribute('multiple')) {
// Get multiple values from data attribute
let values = target.parentNode.getAttribute('data-value').split(',');
// Filter empty values
values.forEach(val => val !== "" ? array_val.push(val) : null);
// Set array value
single_val = array_val;
}
// Handle direct values (non-HTMLElement)
else if (type === null) {
single_val = target;
}
// Handle standard input values
else if (target instanceof HTMLElement) {
single_val = target.value;
}
// Return the normalized value
if(single_val !== undefined) return single_val;
}
// Usage
const handleChange = (target) => {
const value = PhenixBlocks.valueHandler(target);
// Process the normalized value
setAttributes({ [target.name]: value });
};
objectToAttributes
Converts a nested object into a flat attributes object.
/**
* Converts a nested object into a flat attributes object
*
* @param {Object} obj - The nested object to convert
* @param {String} prefix - Optional prefix for attribute names
* @returns {Object} - Flat attributes object
*/
function objectToAttributes(obj, prefix = '') {
const attributes = {};
Object.entries(obj).forEach(([key, value]) => {
const attributeName = prefix ? `${prefix}-${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Recursively flatten nested objects
const nestedAttributes = objectToAttributes(value, attributeName);
Object.assign(attributes, nestedAttributes);
} else {
// Add simple attribute
attributes[attributeName] = value;
}
});
return attributes;
}
// Usage
const styleObject = {
background: {
type: 'color',
value: '#ffffff'
},
radius: '10px'
};
const flatAttributes = PhenixBlocks.objectToAttributes(styleObject, 'style');
// Result: { 'style-background-type': 'color', 'style-background-value': '#ffffff', 'style-radius': '10px' }
attributesToObject
Converts a flat attributes object into a nested object.
/**
* Converts a flat attributes object into a nested object
*
* @param {Object} attributes - The flat attributes object
* @param {String} prefix - The prefix to look for
* @returns {Object} - Nested object
*/
function attributesToObject(attributes, prefix) {
const obj = {};
Object.entries(attributes).forEach(([key, value]) => {
if (key.startsWith(prefix + '-')) {
// Remove prefix
const path = key.substring(prefix.length + 1).split('-');
// Build nested object
let current = obj;
path.forEach((segment, index) => {
if (index === path.length - 1) {
// Set value at leaf node
current[segment] = value;
} else {
// Create nested object if it doesn't exist
current[segment] = current[segment] || {};
current = current[segment];
}
});
}
});
return obj;
}
// Usage
const flatAttributes = {
'style-background-type': 'color',
'style-background-value': '#ffffff',
'style-radius': '10px'
};
const styleObject = PhenixBlocks.attributesToObject(flatAttributes, 'style');
// Result: { background: { type: 'color', value: '#ffffff' }, radius: '10px' }
cssToObject
Converts a CSS string into a style object.
/**
* Converts a CSS string into a style object
*
* @param {String} css - CSS string
* @returns {Object} - Style object
*/
function cssToObject(css) {
const styleObject = {};
if (!css) return styleObject;
// Remove comments and whitespace
const cleanCss = css.replace(/\/\*[\s\S]*?\*\//g, '').trim();
// Split into declarations
const declarations = cleanCss.split(';');
declarations.forEach(declaration => {
const [property, value] = declaration.split(':').map(item => item.trim());
if (property && value) {
// Convert kebab-case to camelCase
const camelProperty = property.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
styleObject[camelProperty] = value;
}
});
return styleObject;
}
// Usage
const cssString = 'background-color: #ffffff; border-radius: 10px; font-size: 16px;';
const styleObject = PhenixBlocks.cssToObject(cssString);
// Result: { backgroundColor: '#ffffff', borderRadius: '10px', fontSize: '16px' }
objectToCss
Converts a style object into a CSS string.
/**
* Converts a style object into a CSS string
*
* @param {Object} styleObject - Style object
* @returns {String} - CSS string
*/
function objectToCss(styleObject) {
if (!styleObject || Object.keys(styleObject).length === 0) return '';
return Object.entries(styleObject)
.map(([property, value]) => {
// Convert camelCase to kebab-case
const kebabProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
return `${kebabProperty}: ${value};`;
})
.join(' ');
}
// Usage
const styleObject = {
backgroundColor: '#ffffff',
borderRadius: '10px',
fontSize: '16px'
};
const cssString = PhenixBlocks.objectToCss(styleObject);
// Result: 'background-color: #ffffff; border-radius: 10px; font-size: 16px;'
Data Validators
validateColor
Validates and normalizes color values.
/**
* Validates and normalizes color values
*
* @param {String} color - Color value to validate
* @returns {String|null} - Normalized color or null if invalid
*/
function validateColor(color) {
if (!color) return null;
// Check if it's a hex color
if (/^#([0-9A-F]{3}){1,2}$/i.test(color)) {
return color;
}
// Check if it's an RGB color
if (/^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/i.test(color)) {
return color;
}
// Check if it's an RGBA color
if (/^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/i.test(color)) {
return color;
}
// Check if it's a named color
const namedColors = ['black', 'white', 'red', 'green', 'blue', 'yellow', 'purple', 'gray', 'orange'];
if (namedColors.includes(color.toLowerCase())) {
return color;
}
return null;
}
// Usage
const validColor = PhenixBlocks.validateColor('#ff0000');
// Result: '#ff0000'
const invalidColor = PhenixBlocks.validateColor('not-a-color');
// Result: null
validateUrl
Validates URL strings.
/**
* Validates URL strings
*
* @param {String} url - URL to validate
* @returns {Boolean} - Whether the URL is valid
*/
function validateUrl(url) {
if (!url) return false;
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
// Usage
const validUrl = PhenixBlocks.validateUrl('https://example.com');
// Result: true
const invalidUrl = PhenixBlocks.validateUrl('not-a-url');
// Result: false
validateNumber
Validates and normalizes number values.
/**
* Validates and normalizes number values
*
* @param {String|Number} value - Value to validate
* @param {Number} min - Minimum allowed value
* @param {Number} max - Maximum allowed value
* @param {Number} defaultValue - Default value if invalid
* @returns {Number} - Normalized number
*/
function validateNumber(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, defaultValue = 0) {
// Convert to number
const num = parseFloat(value);
// Check if it's a valid number
if (isNaN(num)) {
return defaultValue;
}
// Clamp to min/max
return Math.min(Math.max(num, min), max);
}
// Usage
const validNumber = PhenixBlocks.validateNumber('10', 0, 100, 50);
// Result: 10
const tooLarge = PhenixBlocks.validateNumber('200', 0, 100, 50);
// Result: 100
const invalid = PhenixBlocks.validateNumber('not-a-number', 0, 100, 50);
// Result: 50
Data Filters
filterObjectByKeys
Filters an object to only include specific keys.
/**
* Filters an object to only include specific keys
*
* @param {Object} obj - Object to filter
* @param {Array} keys - Keys to include
* @returns {Object} - Filtered object
*/
function filterObjectByKeys(obj, keys) {
if (!obj || typeof obj !== 'object') return {};
return Object.fromEntries(
Object.entries(obj).filter(([key]) => keys.includes(key))
);
}
// Usage
const fullObject = {
title: 'Hello World',
content: 'Lorem ipsum',
author: 'John Doe',
date: '2023-01-01'
};
const filteredObject = PhenixBlocks.filterObjectByKeys(fullObject, ['title', 'content']);
// Result: { title: 'Hello World', content: 'Lorem ipsum' }
deepMerge
Deeply merges two objects.
/**
* Deeply merges two objects
*
* @param {Object} target - Target object
* @param {Object} source - Source object
* @returns {Object} - Merged object
*/
function deepMerge(target, source) {
if (!source || typeof source !== 'object') return target;
if (!target || typeof target !== 'object') return source;
const result = { ...target };
Object.entries(source).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Recursively merge nested objects
result[key] = deepMerge(result[key] || {}, value);
} else {
// Replace or add simple values
result[key] = value;
}
});
return result;
}
// Usage
const defaultStyles = {
typography: {
fontSize: '16px',
color: '#000000'
},
spacing: {
margin: '10px',
padding: '5px'
}
};
const customStyles = {
typography: {
color: '#ff0000'
},
spacing: {
padding: '10px'
}
};
const mergedStyles = PhenixBlocks.deepMerge(defaultStyles, customStyles);
// Result: {
// typography: {
// fontSize: '16px',
// color: '#ff0000'
// },
// spacing: {
// margin: '10px',
// padding: '10px'
// }
// }
Usage Examples
Transforming Data for API Requests
// In your edit.js file
const saveBlockData = () => {
// Get block data
const blockData = {
title: attributes.title,
content: attributes.content,
style: attributes.style
};
// Transform to flat structure for API
const apiData = PhenixBlocks.objectToAttributes(blockData);
// Send to API
apiFetch({
path: '/pds-blocks/v2/save-block',
method: 'POST',
data: apiData
}).then(response => {
console.log('Block saved:', response);
});
};
Validating User Input
// In your edit.js file
const validateInput = (target) => {
const { name, value } = target;
if (name === 'fontSize') {
// Validate number
const validValue = PhenixBlocks.validateNumber(value, 8, 72, 16);
setAttributes({ fontSize: validValue });
} else if (name === 'color') {
// Validate color
const validColor = PhenixBlocks.validateColor(value);
if (validColor) {
setAttributes({ color: validColor });
}
} else if (name === 'url') {
// Validate URL
if (PhenixBlocks.validateUrl(value)) {
setAttributes({ url: value });
}
} else {
// Default setter
setAttributes({ [name]: value });
}
};
Converting CSS to Style Object
// In your edit.js file
const importStyles = (cssString) => {
// Convert CSS to style object
const styleObject = PhenixBlocks.cssToObject(cssString);
// Update attributes
setAttributes({
style: {
...attributes.style,
custom: styleObject
}
});
};
// Then use it with a textarea
<PhenixTextarea
name="customCSS"
label={__("Custom CSS", "pds-blocks")}
value={attributes.customCSSString}
onChange={(target) => {
setAttributes({ customCSSString: target.value });
importStyles(target.value);
}}
/>
Integration with Block Attributes
Data helpers are typically used to process attributes before they are set or after they are retrieved:
// In your edit.js file
const set_value = (target) => {
const { name, value } = target;
// Validate and transform value
let processedValue = value;
if (name === 'fontSize') {
processedValue = PhenixBlocks.validateNumber(value, 8, 72, 16);
} else if (name === 'color') {
processedValue = PhenixBlocks.validateColor(value) || '';
}
// Set attribute
setAttributes({ [name]: processedValue });
};
// Then use it with controls
<PhenixInput
name="fontSize"
label={__("Font Size", "pds-blocks")}
value={attributes.fontSize}
onChange={set_value}
/>
Best Practices
- Input Validation: Always validate user input before setting attributes.
- Data Transformation: Transform data to the appropriate format before using it.
- Default Values: Provide sensible default values for invalid input.
- Error Handling: Handle errors gracefully and provide feedback to users.
- Reusable Functions: Create reusable helper functions for common data operations.
- Type Safety: Ensure that data types are consistent and appropriate for their use.
- Performance: Optimize data transformations for performance, especially for large datasets.