Welcome to OpenFields! This guide covers everything you need to know to contribute, extend, or understand the system.
📖 See Also: Documentation Index | Architecture | Build Process | Admin System
- Plugin Structure - Architecture, database schema, API endpoints
- Admin System - React UI, state management, field type system
- Architecture - System design and database schema
- Build System - Building and releasing the plugin
- Vision & Roadmap - Project goals and current status
- Node.js 16+ & npm/pnpm
- Docker (for wp-env local development)
- WordPress 6.0+
- PHP 7.4+
# Clone repository
git clone https://github.com/novincode/openfields.git
cd openfields
# Start WordPress environment
npm run wp-env start
# In separate terminal: Watch for admin build changes
cd admin
npm install
npm run dev
# In another terminal: Trigger admin build
cd admin
npm run buildAccess:
- WordPress: http://localhost:8888
- WP Admin: http://localhost:8888/wp-admin
- OpenFields: http://localhost:8888/wp-admin?page=openfields
- Go to Plugins page in WordPress admin
- Find "OpenFields" in plugin list
- Click Activate
- Visit Tools → OpenFields to start
┌─────────────────────────────────────────┐
│ WordPress Posts/Pages (Frontend) │
│ - Display field values to site users │
│ - Load from postmeta (of_ prefix) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ WordPress Admin - Meta Boxes │
│ - User fills in field values on edit │
│ - Saved to postmeta on save_post │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ OpenFields Admin Interface (React) │
│ - Create fieldsets & fields │
│ - Configure location rules │
│ - Manage field-specific settings │
│ - All changes sent via REST API │
└─────────────────────────────────────────┘
When user edits a page in WordPress:
POST edit screen loaded
↓
add_meta_boxes action fires
↓
OpenFields_Meta_Box::register_meta_boxes()
↓
Get context: { post_type: 'page', post_id: 5, page_template: 'default', ... }
↓
OpenFields_Location_Manager::get_fieldsets_for_context()
↓
Query all active fieldsets
↓
For each fieldset, check: Do location rules match context?
↓
Rule matching example:
Rule: post_type == 'page' AND page_template == 'default'
Context: { post_type: 'page', page_template: 'default' }
Result: ✓ MATCH - show meta box
↓
add_meta_box() registers matched fieldsets
↓
Gutenberg/Classic Editor displays meta boxes
When user configures a field:
Frontend: User enters field settings in FieldsetEditor
↓
Store: updateFieldLocal() stages changes in pendingFieldChanges Map
↓
User clicks Save Changes button
↓
saveAllChanges() batch operation:
- DELETE fields in pendingFieldDeletions
- POST new fields in pendingFieldAdditions
- PUT modified fields in pendingFieldChanges
↓
API: All requests sent to REST endpoints
↓
Backend: Updates wp_openfields_fields table
↓
Store: Clears pending changes, marks as saved
↓
UI: Toast notification "Saved successfully"
Step 1: Backend - Register Field Type
// In OpenFields_Field_Registry class
public function get_field_types() {
return array(
// ... existing types
'my_field' => array(
'label' => 'My Custom Field',
'icon' => 'icon-name',
'description' => 'Description of field',
'category' => 'advanced',
),
);
}Step 2: Frontend - Create Settings Component
// admin/src/fields/MyFieldSettings.tsx
import { Field } from '../types';
interface MyFieldSettingsProps {
field: Field;
onUpdate: (field: Partial<Field>) => void;
}
export function MyFieldSettings({ field, onUpdate }: MyFieldSettingsProps) {
const settings = field.settings || {};
const handleChange = (key: string, value: any) => {
onUpdate({
...field,
settings: { ...settings, [key]: value }
});
};
return (
<div>
<label>
<input
type="checkbox"
checked={settings.my_option || false}
onChange={(e) => handleChange('my_option', e.target.checked)}
/>
Enable my option
</label>
</div>
);
}Step 3: Frontend - Register in Registry
// admin/src/fields/index.ts
import { MyFieldSettings } from './MyFieldSettings';
export const fieldSettingsRegistry = {
// ... existing types
my_field: MyFieldSettings,
};Step 4: Backend - Render in Meta Box
// In OpenFields_Meta_Box::render_field_input()
case 'my_field':
// Render your custom HTML
echo '<input type="text" name="' . esc_attr($field_name) . '" value="' . esc_attr($value) . '" />';
break;Step 1: Register Location Type
// In OpenFields_Location_Manager::__construct()
$this->register_location_type(
'my_location',
array(
'label' => __('My Location Type', 'openfields'),
'callback' => array($this, 'match_my_location'),
'options' => array($this, 'get_my_location_options'),
)
);Step 2: Implement Matcher Callback
public function match_my_location($value, $operator, $context) {
$current = $context['my_field'] ?? '';
return $this->compare($current, $value, $operator);
}Step 3: Implement Options Callback
public function get_my_location_options() {
return array(
array('name' => 'value1', 'label' => 'Value 1'),
array('name' => 'value2', 'label' => 'Value 2'),
);
}Step 4: Update Context in Meta Box
// In OpenFields_Meta_Box::register_meta_boxes()
$context = array(
'post_type' => $post_type,
'post_id' => $post->ID,
'my_field' => get_my_field_value(), // Add your field
// ... existing fields
);Enable WordPress Debug Mode
// wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);Check Debug Log
cd openfields
npm run wp-env run cli tail -f /var/www/html/wp-content/debug.logDatabase Queries
# List all fieldsets
npm run wp-env run cli wp db query "SELECT * FROM wp_openfields_fieldsets"
# List all fields for fieldset 2
npm run wp-env run cli wp db query "SELECT * FROM wp_openfields_fields WHERE fieldset_id = 2"
# List all location rules for fieldset 2
npm run wp-env run cli wp db query "SELECT * FROM wp_openfields_locations WHERE fieldset_id = 2"React DevTools
Install React DevTools Chrome extension. Then in browser:
- Open DevTools (F12)
- Go to "React" tab
- Inspect components in real-time
- Check Zustand store state
API Debugging
Use REST API debug endpoint:
# Get all fieldsets and locations
curl http://localhost:8888/wp-json/openfields/v1/debug/locationsOr visit directly: http://localhost:8888/wp-json/openfields/v1/debug/locations
-
Via Admin UI:
- Click "New Fieldset"
- Enter title & description
- Add fields (text, email, textarea, etc.)
- Configure location rules
- Click "Save Changes"
-
Via CLI (for quick iteration):
# Create fieldset npm run wp-env run cli wp openfields fieldset create \ --title="Test" \ --description="Test fieldset" # Add field npm run wp-env run cli wp openfields field create \ --fieldset-id=1 \ --label="Test Field" \ --type="text"
- Create fieldset with location rule:
post_type == page - Go to Pages → Add New
- Scroll down → Should see meta box
- Go to Posts → Add New
- Scroll down → Should NOT see meta box
- Add a page with the fieldset
- Fill in field values
- Click "Publish" or "Save"
- Reload page
- Verify field values are still filled
- Check postmeta:
wp_postmetatable should haveof_FIELDNAMEentries
- Lightweight (1KB)
- No boilerplate like Redux
- Good TypeScript support
- Good for staging changes locally
- Rapidly build UI
- Consistent design
- Accessible components (Radix under hood)
- Easy to customize
- WordPress native
- Works with existing tools
- Easy to debug (just HTTP)
- Stateless
- Better UX - undo possible until save
- Batch operations - single HTTP request
- Atomic saves - all or nothing
- Reduced API load
-
Use selectors in components:
const fieldsets = useFieldsetStore((state) => state.fieldsets); // NOT: const store = useFieldsetStore(); store.fieldsets
This prevents re-renders when other store values change.
-
Memoize callbacks:
const handleSave = useCallback(() => { /* ... */ }, [dependencies]);
-
Lazy load pages:
const FieldsetEditor = lazy(() => import('./FieldsetEditor'));
-
Cache location options: Currently fetched every time, could be cached with TTL.
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
IE11 not supported (uses ES2020+, optional chaining, nullish coalescing).
- WordPress Coding Standards
- 4-space indentation
- DocBlocks on all functions/classes
- Use
error_log()for debugging
- ESLint config in place
- Prettier formatting
- 2-space indentation
- Strict mode enabled
- No
anytypes when possible
# Create feature branch
git checkout -b feature/field-type-xyz
# Make changes, commit frequently
git commit -m "Add XYZ field type"
# Push and create PR
git push origin feature/field-type-xyz
# PR triggers CI checks:
# - ESLint
# - TypeScript
# - Build verification- Update
OPENFIELDS_VERSIONinopenfields.php - Update
versioninadmin/package.json - Run
npm run buildin admin folder - Test all field types
- Test location matching
- Test on both Gutenberg and Classic Editor
- Update CHANGELOG.md
- Create git tag:
git tag v0.2.0 - Push tag:
git push origin v0.2.0
- WordPress Plugin Dev Handbook
- REST API Handbook
- WordPress Coding Standards
- React Docs
- TypeScript Docs
- Zustand Docs
- Tailwind Docs
- Check existing issues on GitHub
- Review test fieldsets in
/docs/examples - Look at existing field types for patterns
- Check debug.log for errors
- Ask in discussions or open an issue
- Fork the repository
- Create feature branch
- Make changes with tests
- Submit PR with description
- Address review feedback
- Merge when approved
See CONTRIBUTING.md for detailed guidelines.