/**
* Proto-Blocks Interactivity Knowledge Module
* Complete documentation for adding frontend interactivity
*/
export function getInteractivityKnowledge(approach) {
if (approach === 'plain-js') {
return getPlainJsGuide();
}
if (approach === 'es-modules') {
return getEsModulesGuide();
}
if (approach === 'interactivity-api') {
return getInteractivityApiGuide();
}
return `# Proto-Blocks Interactivity: Complete Guide
Add frontend interactivity to your Proto-Blocks using JavaScript.
---
## Approaches Overview
| Approach | Complexity | Best For |
|----------|------------|----------|
| Plain JavaScript | Low | Simple interactions |
| ES Modules | Medium | Organized code |
| Interactivity API | High | Reactive UIs |
---
## 1. Plain JavaScript
Traditional JavaScript for simple interactions.
### Setup
Create \`view.js\` in your block folder:
\`\`\`
my-block/
├── block.json
├── template.php
├── style.css
└── view.js ← Frontend JavaScript
\`\`\`
### block.json
\`\`\`json
{
"viewScript": "file:./view.js"
}
\`\`\`
### view.js
\`\`\`javascript
document.addEventListener('DOMContentLoaded', function() {
const blocks = document.querySelectorAll('.my-accordion');
blocks.forEach(function(block) {
const items = block.querySelectorAll('.accordion-item');
items.forEach(function(item) {
const trigger = item.querySelector('.accordion-trigger');
trigger.addEventListener('click', function() {
item.classList.toggle('is-open');
});
});
});
});
\`\`\`
### Use Cases
- Toggle visibility
- Simple animations
- Form validation
- Click handlers
---
## 2. ES Modules
Modern JavaScript with module syntax.
### block.json
\`\`\`json
{
"viewScriptModule": "file:./view.js"
}
\`\`\`
Note: Use \`viewScriptModule\` (not \`viewScript\`) for ES modules.
### view.js
\`\`\`javascript
// No DOMContentLoaded needed with modules
const accordions = document.querySelectorAll('.my-accordion');
accordions.forEach(accordion => {
initAccordion(accordion);
});
function initAccordion(container) {
const items = container.querySelectorAll('.accordion-item');
items.forEach(item => {
const trigger = item.querySelector('.accordion-trigger');
const content = item.querySelector('.accordion-content');
trigger?.addEventListener('click', () => {
const isOpen = item.classList.contains('is-open');
// Close all items
items.forEach(i => i.classList.remove('is-open'));
// Toggle clicked item
if (!isOpen) {
item.classList.add('is-open');
}
});
});
}
\`\`\`
### Use Cases
- Modular code organization
- Import/export functionality
- Modern JavaScript features
- Better code splitting
---
## 3. WordPress Interactivity API
Reactive state management for complex UIs.
### block.json
\`\`\`json
{
"supports": {
"interactivity": true
},
"viewScriptModule": "file:./view.js"
}
\`\`\`
### template.php
\`\`\`php
<?php
$items = $attributes['items'] ?? [];
$allow_multiple = $attributes['allowMultiple'] ?? false;
$first_open = $attributes['firstOpen'] ?? true;
// Initial context state
$context = [
'allowMultiple' => $allow_multiple,
'openItems' => $first_open ? [0] : [],
];
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'accordion',
'data-wp-interactive' => 'proto-blocks/accordion',
'data-wp-context' => wp_json_encode($context),
]);
?>
<div <?php echo $wrapper_attributes; ?> data-proto-repeater="items">
<?php foreach ($items as $index => $item) : ?>
<div
class="accordion-item"
data-proto-repeater-item
data-wp-context='{"index": <?php echo $index; ?>}'
data-wp-class--is-open="state.isItemOpen"
>
<button
class="accordion-trigger"
data-wp-on--click="actions.toggle"
>
<span data-proto-field="title">
<?php echo esc_html($item['title'] ?? ''); ?>
</span>
<span data-wp-text="state.icon"></span>
</button>
<div
class="accordion-content"
data-wp-bind--hidden="!state.isItemOpen"
>
<div data-proto-field="content">
<?php echo wp_kses_post($item['content'] ?? ''); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
\`\`\`
### view.js
\`\`\`javascript
import { store, getContext } from '@wordpress/interactivity';
store('proto-blocks/accordion', {
state: {
get isItemOpen() {
const { openItems } = getContext();
const { index } = getContext();
return openItems.includes(index);
},
get icon() {
const { openItems } = getContext();
const { index } = getContext();
return openItems.includes(index) ? '−' : '+';
},
},
actions: {
toggle() {
const context = getContext();
const { index } = context;
const { allowMultiple } = context;
if (context.openItems.includes(index)) {
// Close this item
context.openItems = context.openItems.filter(i => i !== index);
} else {
// Open this item
if (allowMultiple) {
context.openItems = [...context.openItems, index];
} else {
context.openItems = [index];
}
}
},
},
});
\`\`\`
### Interactivity API Directives
| Directive | Purpose | Example |
|-----------|---------|---------|
| \`data-wp-interactive\` | Initialize store | \`"namespace/store"\` |
| \`data-wp-context\` | Scoped state | \`'{"key": "value"}'\` |
| \`data-wp-on--{event}\` | Event handler | \`"actions.handleClick"\` |
| \`data-wp-bind--{attr}\` | Attribute binding | \`"state.isVisible"\` |
| \`data-wp-class--{class}\` | Class binding | \`"state.isActive"\` |
| \`data-wp-text\` | Text content | \`"state.label"\` |
| \`data-wp-html\` | HTML content | \`"state.htmlContent"\` |
### Use Cases
- Accordions / Tabs
- Modals / Dialogs
- Carousels / Sliders
- Shopping carts
- Real-time updates
---
## Choosing the Right Approach
### Use Plain JavaScript When:
- Simple toggle/show/hide
- Basic event handling
- No state management needed
- Quick implementation
### Use ES Modules When:
- Code organization matters
- Sharing code between files
- Modern JavaScript syntax
- Medium complexity
### Use Interactivity API When:
- Complex state management
- Reactive UI updates
- Multiple components sharing state
- WordPress-native approach
---
## Template with All Three Options
### Accordion Example
**Plain JavaScript version:**
\`\`\`javascript
// view.js
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.accordion-trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
trigger.closest('.accordion-item').classList.toggle('is-open');
});
});
});
\`\`\`
**ES Modules version:**
\`\`\`javascript
// view.js
document.querySelectorAll('.accordion').forEach(accordion => {
const allowMultiple = accordion.dataset.allowMultiple === 'true';
accordion.querySelectorAll('.accordion-trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
const item = trigger.closest('.accordion-item');
if (!allowMultiple) {
accordion.querySelectorAll('.accordion-item.is-open')
.forEach(openItem => {
if (openItem !== item) openItem.classList.remove('is-open');
});
}
item.classList.toggle('is-open');
});
});
});
\`\`\`
**Interactivity API version:**
\`\`\`javascript
// view.js
import { store, getContext } from '@wordpress/interactivity';
store('proto-blocks/accordion', {
state: {
get isOpen() {
const ctx = getContext();
return ctx.openItems.includes(ctx.index);
}
},
actions: {
toggle() {
const ctx = getContext();
if (ctx.openItems.includes(ctx.index)) {
ctx.openItems = ctx.openItems.filter(i => i !== ctx.index);
} else {
ctx.openItems = ctx.allowMultiple
? [...ctx.openItems, ctx.index]
: [ctx.index];
}
}
}
});
\`\`\`
---
## Best Practices
1. **Progressive Enhancement**: Ensure content works without JS
2. **Scoped Selectors**: Target specific block instances
3. **Event Delegation**: For dynamic content
4. **Cleanup**: Remove event listeners when needed
5. **Error Handling**: Wrap code in try/catch
6. **Performance**: Debounce/throttle expensive operations
`;
}
function getPlainJsGuide() {
return `# Plain JavaScript Interactivity
Traditional JavaScript for simple Proto-Blocks interactions.
## Setup
Create \`view.js\` in your block folder and reference it in block.json:
\`\`\`json
{
"viewScript": "file:./view.js"
}
\`\`\`
## Basic Pattern
\`\`\`javascript
document.addEventListener('DOMContentLoaded', function() {
// Find all instances of your block
const blocks = document.querySelectorAll('.wp-block-proto-blocks-my-block');
blocks.forEach(function(block) {
// Initialize each block
initBlock(block);
});
});
function initBlock(block) {
// Your initialization code
const button = block.querySelector('.my-button');
button.addEventListener('click', function(event) {
event.preventDefault();
// Handle click
});
}
\`\`\`
## Toggle Example
\`\`\`javascript
document.addEventListener('DOMContentLoaded', function() {
const toggles = document.querySelectorAll('.toggle-trigger');
toggles.forEach(function(toggle) {
toggle.addEventListener('click', function() {
const target = document.getElementById(this.dataset.target);
target.classList.toggle('is-visible');
this.setAttribute(
'aria-expanded',
target.classList.contains('is-visible')
);
});
});
});
\`\`\`
## Modal Example
\`\`\`javascript
document.addEventListener('DOMContentLoaded', function() {
// Open modal
document.querySelectorAll('[data-modal-open]').forEach(function(trigger) {
trigger.addEventListener('click', function() {
const modal = document.getElementById(this.dataset.modalOpen);
modal.classList.add('is-open');
document.body.classList.add('modal-open');
});
});
// Close modal
document.querySelectorAll('[data-modal-close]').forEach(function(close) {
close.addEventListener('click', function() {
const modal = this.closest('.modal');
modal.classList.remove('is-open');
document.body.classList.remove('modal-open');
});
});
// Close on escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal.is-open').forEach(function(modal) {
modal.classList.remove('is-open');
});
document.body.classList.remove('modal-open');
}
});
});
\`\`\`
## Tips
1. Always use \`DOMContentLoaded\` for viewScript
2. Use event delegation for dynamic content
3. Query within the block context, not globally
4. Use data attributes for configuration
`;
}
function getEsModulesGuide() {
return `# ES Modules Interactivity
Modern JavaScript modules for Proto-Blocks.
## Setup
Use \`viewScriptModule\` instead of \`viewScript\`:
\`\`\`json
{
"viewScriptModule": "file:./view.js"
}
\`\`\`
## Basic Pattern
\`\`\`javascript
// Modules are deferred by default, no DOMContentLoaded needed
const blocks = document.querySelectorAll('.wp-block-proto-blocks-my-block');
blocks.forEach(block => {
initBlock(block);
});
function initBlock(block) {
const button = block.querySelector('.my-button');
button?.addEventListener('click', (event) => {
event.preventDefault();
handleClick(event.target);
});
}
function handleClick(element) {
console.log('Clicked:', element);
}
\`\`\`
## Class-Based Pattern
\`\`\`javascript
class Accordion {
constructor(element) {
this.element = element;
this.items = element.querySelectorAll('.accordion-item');
this.allowMultiple = element.dataset.allowMultiple === 'true';
this.init();
}
init() {
this.items.forEach(item => {
const trigger = item.querySelector('.accordion-trigger');
trigger?.addEventListener('click', () => this.toggle(item));
});
}
toggle(item) {
const isOpen = item.classList.contains('is-open');
if (!this.allowMultiple) {
this.closeAll();
}
item.classList.toggle('is-open', !isOpen);
}
closeAll() {
this.items.forEach(item => {
item.classList.remove('is-open');
});
}
}
// Initialize
document.querySelectorAll('.accordion').forEach(el => {
new Accordion(el);
});
\`\`\`
## Importing WordPress Interactivity
\`\`\`javascript
import { store, getContext } from '@wordpress/interactivity';
// Use with Interactivity API
store('my-namespace/my-store', {
// Store definition
});
\`\`\`
## Benefits
- Clean, organized code
- Modern JavaScript syntax
- Automatic deferring
- Better tooling support
`;
}
function getInteractivityApiGuide() {
return `# WordPress Interactivity API Guide
Reactive state management for Proto-Blocks.
## Requirements
1. WordPress 6.5+
2. \`supports.interactivity: true\` in block.json
3. \`viewScriptModule\` for your JavaScript
## Setup
### block.json
\`\`\`json
{
"name": "proto-blocks/tabs",
"supports": {
"interactivity": true
},
"viewScriptModule": "file:./view.js",
"protoBlocks": {
"fields": {
"tabs": {
"type": "repeater",
"fields": {
"title": { "type": "text" },
"content": { "type": "wysiwyg" }
}
}
}
}
}
\`\`\`
## Template Pattern
\`\`\`php
<?php
$tabs = $attributes['tabs'] ?? [];
$context = [
'activeTab' => 0,
];
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'tabs',
'data-wp-interactive' => 'proto-blocks/tabs',
'data-wp-context' => wp_json_encode($context),
]);
?>
<div <?php echo $wrapper_attributes; ?>>
<!-- Tab Buttons -->
<div class="tabs__nav" role="tablist">
<?php foreach ($tabs as $index => $tab) : ?>
<button
class="tabs__button"
role="tab"
data-wp-context='{"index": <?php echo $index; ?>}'
data-wp-class--is-active="state.isActiveTab"
data-wp-on--click="actions.selectTab"
data-wp-bind--aria-selected="state.isActiveTab"
>
<span data-proto-field="title">
<?php echo esc_html($tab['title'] ?? ''); ?>
</span>
</button>
<?php endforeach; ?>
</div>
<!-- Tab Panels -->
<div class="tabs__panels" data-proto-repeater="tabs">
<?php foreach ($tabs as $index => $tab) : ?>
<div
class="tabs__panel"
role="tabpanel"
data-proto-repeater-item
data-wp-context='{"index": <?php echo $index; ?>}'
data-wp-bind--hidden="!state.isActiveTab"
>
<div data-proto-field="content">
<?php echo wp_kses_post($tab['content'] ?? ''); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
\`\`\`
## view.js
\`\`\`javascript
import { store, getContext } from '@wordpress/interactivity';
store('proto-blocks/tabs', {
state: {
get isActiveTab() {
const context = getContext();
return context.activeTab === context.index;
},
},
actions: {
selectTab() {
const context = getContext();
context.activeTab = context.index;
},
},
});
\`\`\`
## Directives Reference
### data-wp-interactive
Initialize the interactive store:
\`\`\`html
<div data-wp-interactive="namespace/store-name">
\`\`\`
### data-wp-context
Set scoped context (state accessible to this element and children):
\`\`\`html
<div data-wp-context='{"count": 0, "isOpen": false}'>
\`\`\`
### data-wp-on--{event}
Attach event handlers:
\`\`\`html
<button data-wp-on--click="actions.handleClick">
<input data-wp-on--input="actions.handleInput">
<form data-wp-on--submit="actions.handleSubmit">
\`\`\`
### data-wp-bind--{attribute}
Bind attributes reactively:
\`\`\`html
<div data-wp-bind--hidden="!state.isVisible">
<input data-wp-bind--value="state.inputValue">
<img data-wp-bind--src="state.imageUrl">
<a data-wp-bind--href="state.linkUrl">
\`\`\`
### data-wp-class--{class}
Toggle classes reactively:
\`\`\`html
<div data-wp-class--is-active="state.isActive">
<div data-wp-class--is-loading="state.isLoading">
\`\`\`
### data-wp-text
Set text content:
\`\`\`html
<span data-wp-text="state.count"></span>
<p data-wp-text="state.message"></p>
\`\`\`
### data-wp-html
Set HTML content (use carefully):
\`\`\`html
<div data-wp-html="state.richContent"></div>
\`\`\`
## Store Definition
\`\`\`javascript
import { store, getContext, getElement } from '@wordpress/interactivity';
store('namespace/store', {
// Reactive state (getters)
state: {
get computedValue() {
const ctx = getContext();
return ctx.someValue * 2;
},
},
// Actions (event handlers)
actions: {
increment() {
const ctx = getContext();
ctx.count++;
},
async fetchData() {
const ctx = getContext();
ctx.isLoading = true;
try {
const response = await fetch('/api/data');
ctx.data = await response.json();
} finally {
ctx.isLoading = false;
}
},
},
// Side effects
callbacks: {
logChange() {
const ctx = getContext();
console.log('Value changed:', ctx.count);
},
},
});
\`\`\`
## Complete Accordion Example
### block.json
\`\`\`json
{
"name": "proto-blocks/accordion",
"supports": { "interactivity": true },
"viewScriptModule": "file:./view.js",
"protoBlocks": {
"fields": {
"items": {
"type": "repeater",
"fields": {
"title": { "type": "text" },
"content": { "type": "wysiwyg" }
}
}
},
"controls": {
"allowMultiple": {
"type": "toggle",
"label": "Allow Multiple Open",
"default": false
}
}
}
}
\`\`\`
### template.php
\`\`\`php
<?php
$items = $attributes['items'] ?? [];
$allow_multiple = $attributes['allowMultiple'] ?? false;
$context = [
'allowMultiple' => $allow_multiple,
'openItems' => [],
];
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'accordion',
'data-wp-interactive' => 'proto-blocks/accordion',
'data-wp-context' => wp_json_encode($context),
]);
?>
<div <?php echo $wrapper_attributes; ?> data-proto-repeater="items">
<?php foreach ($items as $index => $item) : ?>
<div
class="accordion__item"
data-proto-repeater-item
data-wp-context='{"index": <?php echo $index; ?>}'
data-wp-class--is-open="state.isOpen"
>
<button
class="accordion__trigger"
data-wp-on--click="actions.toggle"
data-wp-bind--aria-expanded="state.isOpen"
>
<span data-proto-field="title">
<?php echo esc_html($item['title'] ?? ''); ?>
</span>
<span class="accordion__icon" data-wp-text="state.icon"></span>
</button>
<div
class="accordion__content"
data-wp-bind--hidden="!state.isOpen"
>
<div data-proto-field="content">
<?php echo wp_kses_post($item['content'] ?? ''); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
\`\`\`
### view.js
\`\`\`javascript
import { store, getContext } from '@wordpress/interactivity';
store('proto-blocks/accordion', {
state: {
get isOpen() {
const ctx = getContext();
return ctx.openItems.includes(ctx.index);
},
get icon() {
const ctx = getContext();
return ctx.openItems.includes(ctx.index) ? '−' : '+';
},
},
actions: {
toggle() {
const ctx = getContext();
const { index, allowMultiple, openItems } = ctx;
if (openItems.includes(index)) {
ctx.openItems = openItems.filter(i => i !== index);
} else {
ctx.openItems = allowMultiple
? [...openItems, index]
: [index];
}
},
},
});
\`\`\`
## Tips
1. **Keep Context Small**: Only store what's needed
2. **Use Getters**: For computed/derived state
3. **Scope Actions**: Access context within actions
4. **Avoid DOM**: Let directives handle DOM updates
5. **Test Thoroughly**: Reactive systems can be tricky
`;
}