/**
* Proto-Blocks Repeaters Knowledge Module
* Comprehensive guide for implementing simple to complex repeaters
*/
export function getRepeatersKnowledge() {
return `# Proto-Blocks Repeaters: The Complete Guide
Repeaters are one of the most powerful features in Proto-Blocks, allowing you to create dynamic, repeatable content groups. This comprehensive guide covers everything from basic lists to complex nested structures.
---
## Table of Contents
1. [What Are Repeaters?](#what-are-repeaters)
2. [Basic Repeater Concepts](#basic-repeater-concepts)
3. [Simple Repeater Examples](#simple-repeater-examples)
4. [Intermediate Repeater Patterns](#intermediate-repeater-patterns)
5. [Advanced Repeater Techniques](#advanced-repeater-techniques)
6. [Nested Repeaters](#nested-repeaters)
7. [Repeaters with Complex Fields](#repeaters-with-complex-fields)
8. [Styling Repeaters](#styling-repeaters)
9. [Repeaters with Interactivity](#repeaters-with-interactivity)
10. [Performance Considerations](#performance-considerations)
11. [Common Patterns & Recipes](#common-patterns--recipes)
12. [Troubleshooting Repeaters](#troubleshooting-repeaters)
---
## What Are Repeaters?
Repeaters allow users to add, remove, reorder, and duplicate groups of fields. They're perfect for:
- Feature lists
- Team member grids
- Testimonial carousels
- FAQ accordions
- Pricing tables
- Timeline/step components
- Gallery items
- Menu items
- Any list-based content
### How Repeaters Work
1. **Definition**: You define a repeater field in \`block.json\` with nested field definitions
2. **Storage**: Data is stored as an array of objects
3. **Editor UI**: Users see a sortable list with add/remove/reorder controls
4. **Template**: You loop through items and render each with proper data attributes
---
## Basic Repeater Concepts
### The Three Required Attributes
Every repeater implementation needs these data attributes in your template:
\`\`\`php
<!-- 1. Container: data-proto-repeater="fieldName" -->
<ul data-proto-repeater="items">
<!-- 2. Item wrapper: data-proto-repeater-item -->
<li data-proto-repeater-item>
<!-- 3. Fields inside: data-proto-field="nestedFieldName" -->
<span data-proto-field="title">...</span>
</li>
</ul>
\`\`\`
### Repeater Data Structure
\`\`\`javascript
// What you define in block.json
"items": {
"type": "repeater",
"fields": {
"title": { "type": "text" },
"content": { "type": "wysiwyg" }
}
}
// What gets stored (array of objects)
[
{ "title": "First Item", "content": "<p>Content...</p>" },
{ "title": "Second Item", "content": "<p>More content...</p>" }
]
// What you access in PHP
$items = $attributes['items']; // Array
foreach ($items as $item) {
$item['title']; // "First Item"
$item['content']; // "<p>Content...</p>"
}
\`\`\`
---
## Simple Repeater Examples
### Example 1: Basic Text List
The simplest repeater - a list of text items.
**block.json:**
\`\`\`json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "proto-blocks/simple-list",
"title": "Simple List",
"category": "proto-blocks",
"icon": "list-view",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"items": {
"type": "repeater",
"min": 1,
"max": 20,
"fields": {
"text": {
"type": "text",
"tagName": "span"
}
}
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$items = $attributes['items'] ?? [];
// Default items for empty state
if (empty($items)) {
$items = [
['text' => 'First item'],
['text' => 'Second item'],
['text' => 'Third item'],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'simple-list',
]);
?>
<div <?php echo $wrapper_attributes; ?>>
<ul class="simple-list__items" data-proto-repeater="items">
<?php foreach ($items as $item) : ?>
<li class="simple-list__item" data-proto-repeater-item>
<span data-proto-field="text">
<?php echo esc_html($item['text'] ?? ''); ?>
</span>
</li>
<?php endforeach; ?>
</ul>
</div>
\`\`\`
**style.css:**
\`\`\`css
.simple-list__items {
list-style: none;
padding: 0;
margin: 0;
}
.simple-list__item {
padding: 0.75rem 0;
border-bottom: 1px solid #e5e7eb;
}
.simple-list__item:last-child {
border-bottom: none;
}
\`\`\`
---
### Example 2: Feature List with Icons
A list with icon images and text.
**block.json:**
\`\`\`json
{
"name": "proto-blocks/feature-list",
"title": "Feature List",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"features": {
"type": "repeater",
"min": 1,
"max": 12,
"itemLabel": "title",
"collapsible": true,
"fields": {
"icon": {
"type": "image",
"sizes": ["thumbnail"]
},
"title": {
"type": "text",
"tagName": "h3"
},
"description": {
"type": "wysiwyg"
}
}
}
},
"controls": {
"columns": {
"type": "range",
"label": "Columns",
"min": 1,
"max": 4,
"default": 3
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$features = $attributes['features'] ?? [];
$columns = $attributes['columns'] ?? 3;
// Default features for editor
if (empty($features)) {
$features = [
['icon' => null, 'title' => 'Feature 1', 'description' => ''],
['icon' => null, 'title' => 'Feature 2', 'description' => ''],
['icon' => null, 'title' => 'Feature 3', 'description' => ''],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'feature-list',
'style' => "--columns: {$columns};",
]);
?>
<section <?php echo $wrapper_attributes; ?>>
<div class="feature-list__grid" data-proto-repeater="features">
<?php foreach ($features as $feature) : ?>
<article class="feature-list__item" data-proto-repeater-item>
<div class="feature-list__icon" data-proto-field="icon">
<?php if (!empty($feature['icon']['url'])) : ?>
<img
src="<?php echo esc_url($feature['icon']['url']); ?>"
alt="<?php echo esc_attr($feature['icon']['alt'] ?? ''); ?>"
/>
<?php else : ?>
<span class="feature-list__icon-placeholder">✦</span>
<?php endif; ?>
</div>
<h3 class="feature-list__title" data-proto-field="title">
<?php echo esc_html($feature['title'] ?? ''); ?>
</h3>
<div class="feature-list__description" data-proto-field="description">
<?php echo wp_kses_post($feature['description'] ?? ''); ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
\`\`\`
**style.css:**
\`\`\`css
.feature-list__grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 2rem;
}
@media (max-width: 768px) {
.feature-list__grid {
grid-template-columns: 1fr;
}
}
.feature-list__item {
text-align: center;
padding: 1.5rem;
}
.feature-list__icon {
width: 64px;
height: 64px;
margin: 0 auto 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.feature-list__icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.feature-list__icon-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f1f5f9;
border-radius: 12px;
font-size: 1.5rem;
color: #94a3b8;
}
.feature-list__title {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
}
.feature-list__title:empty::before {
content: 'Enter title';
color: #94a3b8;
}
.feature-list__description {
color: #64748b;
font-size: 0.9375rem;
}
\`\`\`
---
## Intermediate Repeater Patterns
### Example 3: Testimonials with All Field Types
A testimonial slider using text, WYSIWYG, image, and link fields.
**block.json:**
\`\`\`json
{
"name": "proto-blocks/testimonials",
"title": "Testimonials",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"testimonials": {
"type": "repeater",
"min": 1,
"max": 10,
"itemLabel": "authorName",
"collapsible": true,
"fields": {
"quote": {
"type": "wysiwyg"
},
"authorImage": {
"type": "image",
"sizes": ["thumbnail", "medium"]
},
"authorName": {
"type": "text",
"tagName": "strong"
},
"authorTitle": {
"type": "text",
"tagName": "span"
},
"companyLink": {
"type": "link"
}
}
}
},
"controls": {
"layout": {
"type": "select",
"label": "Layout",
"default": "grid",
"options": [
{ "key": "grid", "label": "Grid" },
{ "key": "carousel", "label": "Carousel" },
{ "key": "stack", "label": "Stacked" }
]
},
"showRating": {
"type": "toggle",
"label": "Show Star Rating",
"default": true
},
"rating": {
"type": "range",
"label": "Default Rating",
"min": 1,
"max": 5,
"default": 5,
"conditions": {
"visible": {
"showRating": [true]
}
}
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$testimonials = $attributes['testimonials'] ?? [];
$layout = $attributes['layout'] ?? 'grid';
$show_rating = $attributes['showRating'] ?? true;
$rating = $attributes['rating'] ?? 5;
// Defaults for editor
if (empty($testimonials)) {
$testimonials = [
[
'quote' => '<p>"Amazing product! Highly recommended."</p>',
'authorImage' => null,
'authorName' => 'John Doe',
'authorTitle' => 'CEO, Company',
'companyLink' => null,
],
[
'quote' => '<p>"Changed the way we work."</p>',
'authorImage' => null,
'authorName' => 'Jane Smith',
'authorTitle' => 'Designer',
'companyLink' => null,
],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => "testimonials testimonials--{$layout}",
]);
?>
<section <?php echo $wrapper_attributes; ?>>
<div class="testimonials__container" data-proto-repeater="testimonials">
<?php foreach ($testimonials as $testimonial) : ?>
<figure class="testimonial" data-proto-repeater-item>
<?php if ($show_rating) : ?>
<div class="testimonial__rating">
<?php for ($i = 1; $i <= 5; $i++) : ?>
<span class="star <?php echo $i <= $rating ? 'star--filled' : ''; ?>">★</span>
<?php endfor; ?>
</div>
<?php endif; ?>
<blockquote class="testimonial__quote" data-proto-field="quote">
<?php echo wp_kses_post($testimonial['quote'] ?? ''); ?>
</blockquote>
<figcaption class="testimonial__author">
<div class="testimonial__avatar" data-proto-field="authorImage">
<?php if (!empty($testimonial['authorImage']['url'])) : ?>
<img
src="<?php echo esc_url($testimonial['authorImage']['url']); ?>"
alt="<?php echo esc_attr($testimonial['authorImage']['alt'] ?? ''); ?>"
/>
<?php else : ?>
<span class="testimonial__avatar-placeholder">👤</span>
<?php endif; ?>
</div>
<div class="testimonial__info">
<strong class="testimonial__name" data-proto-field="authorName">
<?php echo esc_html($testimonial['authorName'] ?? ''); ?>
</strong>
<span class="testimonial__title" data-proto-field="authorTitle">
<?php echo esc_html($testimonial['authorTitle'] ?? ''); ?>
</span>
<?php
$link = $testimonial['companyLink'] ?? null;
if ($link && !empty($link['url'])) :
?>
<a
href="<?php echo esc_url($link['url']); ?>"
class="testimonial__link"
data-proto-field="companyLink"
<?php echo !empty($link['target']) ? 'target="' . esc_attr($link['target']) . '"' : ''; ?>
>
<?php echo esc_html($link['text'] ?? 'Visit'); ?>
</a>
<?php else : ?>
<a href="#" class="testimonial__link testimonial__link--placeholder" data-proto-field="companyLink">
Add link
</a>
<?php endif; ?>
</div>
</figcaption>
</figure>
<?php endforeach; ?>
</div>
</section>
\`\`\`
---
### Example 4: Pricing Table
A pricing table with feature repeaters inside each plan.
**block.json:**
\`\`\`json
{
"name": "proto-blocks/pricing-table",
"title": "Pricing Table",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"plans": {
"type": "repeater",
"min": 1,
"max": 5,
"itemLabel": "name",
"collapsible": true,
"fields": {
"name": {
"type": "text",
"tagName": "h3"
},
"price": {
"type": "text",
"tagName": "span"
},
"period": {
"type": "text",
"tagName": "span"
},
"description": {
"type": "wysiwyg"
},
"features": {
"type": "repeater",
"min": 1,
"max": 15,
"itemLabel": "text",
"fields": {
"text": {
"type": "text",
"tagName": "span"
},
"included": {
"type": "text",
"tagName": "span"
}
}
},
"ctaLink": {
"type": "link"
}
}
}
},
"controls": {
"highlightIndex": {
"type": "number",
"label": "Highlighted Plan (1-based)",
"min": 0,
"max": 5,
"default": 2
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$plans = $attributes['plans'] ?? [];
$highlight_index = ($attributes['highlightIndex'] ?? 2) - 1; // Convert to 0-based
// Default plans for editor
if (empty($plans)) {
$plans = [
[
'name' => 'Basic',
'price' => '$9',
'period' => '/month',
'description' => '<p>Perfect for getting started</p>',
'features' => [
['text' => '5 Projects', 'included' => '✓'],
['text' => '1GB Storage', 'included' => '✓'],
['text' => 'Email Support', 'included' => '✓'],
],
'ctaLink' => ['url' => '#', 'text' => 'Get Started'],
],
[
'name' => 'Pro',
'price' => '$29',
'period' => '/month',
'description' => '<p>For growing teams</p>',
'features' => [
['text' => 'Unlimited Projects', 'included' => '✓'],
['text' => '10GB Storage', 'included' => '✓'],
['text' => 'Priority Support', 'included' => '✓'],
['text' => 'Analytics', 'included' => '✓'],
],
'ctaLink' => ['url' => '#', 'text' => 'Get Started'],
],
[
'name' => 'Enterprise',
'price' => '$99',
'period' => '/month',
'description' => '<p>For large organizations</p>',
'features' => [
['text' => 'Everything in Pro', 'included' => '✓'],
['text' => 'Unlimited Storage', 'included' => '✓'],
['text' => 'Dedicated Support', 'included' => '✓'],
['text' => 'Custom Integrations', 'included' => '✓'],
],
'ctaLink' => ['url' => '#', 'text' => 'Contact Sales'],
],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'pricing-table',
]);
?>
<section <?php echo $wrapper_attributes; ?>>
<div class="pricing-table__plans" data-proto-repeater="plans">
<?php foreach ($plans as $index => $plan) : ?>
<?php $is_highlighted = $index === $highlight_index; ?>
<article
class="pricing-plan <?php echo $is_highlighted ? 'pricing-plan--highlighted' : ''; ?>"
data-proto-repeater-item
>
<?php if ($is_highlighted) : ?>
<div class="pricing-plan__badge">Most Popular</div>
<?php endif; ?>
<header class="pricing-plan__header">
<h3 class="pricing-plan__name" data-proto-field="name">
<?php echo esc_html($plan['name'] ?? ''); ?>
</h3>
<div class="pricing-plan__price">
<span class="pricing-plan__amount" data-proto-field="price">
<?php echo esc_html($plan['price'] ?? ''); ?>
</span>
<span class="pricing-plan__period" data-proto-field="period">
<?php echo esc_html($plan['period'] ?? ''); ?>
</span>
</div>
<div class="pricing-plan__description" data-proto-field="description">
<?php echo wp_kses_post($plan['description'] ?? ''); ?>
</div>
</header>
<ul class="pricing-plan__features" data-proto-repeater="features">
<?php foreach ($plan['features'] ?? [] as $feature) : ?>
<li class="pricing-plan__feature" data-proto-repeater-item>
<span class="pricing-plan__feature-check" data-proto-field="included">
<?php echo esc_html($feature['included'] ?? '✓'); ?>
</span>
<span class="pricing-plan__feature-text" data-proto-field="text">
<?php echo esc_html($feature['text'] ?? ''); ?>
</span>
</li>
<?php endforeach; ?>
</ul>
<?php $cta = $plan['ctaLink'] ?? null; ?>
<a
href="<?php echo esc_url($cta['url'] ?? '#'); ?>"
class="pricing-plan__cta"
data-proto-field="ctaLink"
<?php echo !empty($cta['target']) ? 'target="' . esc_attr($cta['target']) . '"' : ''; ?>
>
<?php echo esc_html($cta['text'] ?? 'Get Started'); ?>
</a>
</article>
<?php endforeach; ?>
</div>
</section>
\`\`\`
---
## Advanced Repeater Techniques
### Repeater Configuration Options
\`\`\`json
{
"items": {
"type": "repeater",
// Item count limits
"min": 1, // Minimum items (default: 0)
"max": 10, // Maximum items (default: unlimited)
// Editor UI options
"itemLabel": "title", // Field to use as item label
"collapsible": true, // Allow collapsing items
"defaultCollapsed": false, // Start items collapsed
// Action controls
"allowAdd": true, // Show "Add" button
"allowRemove": true, // Show "Remove" button
"allowReorder": true, // Enable drag-drop
"allowDuplicate": true, // Show "Duplicate" button
// Nested fields
"fields": {
// ... field definitions
}
}
}
\`\`\`
### Dynamic Item Labels
Use \`itemLabel\` to show meaningful labels in the editor sidebar:
\`\`\`json
{
"teamMembers": {
"type": "repeater",
"itemLabel": "name", // Shows member name as item label
"fields": {
"name": { "type": "text" },
"role": { "type": "text" },
"bio": { "type": "wysiwyg" }
}
}
}
\`\`\`
The editor will display:
- "John Doe" instead of "Item 1"
- "Jane Smith" instead of "Item 2"
---
## Nested Repeaters
Nested repeaters allow creating hierarchical data structures.
### Example 5: FAQ with Categories
**block.json:**
\`\`\`json
{
"name": "proto-blocks/faq-categories",
"title": "FAQ with Categories",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"categories": {
"type": "repeater",
"min": 1,
"max": 10,
"itemLabel": "categoryName",
"collapsible": true,
"fields": {
"categoryName": {
"type": "text",
"tagName": "h2"
},
"categoryDescription": {
"type": "wysiwyg"
},
"questions": {
"type": "repeater",
"min": 1,
"max": 20,
"itemLabel": "question",
"collapsible": true,
"fields": {
"question": {
"type": "text",
"tagName": "h3"
},
"answer": {
"type": "wysiwyg"
}
}
}
}
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$categories = $attributes['categories'] ?? [];
// Default structure for editor
if (empty($categories)) {
$categories = [
[
'categoryName' => 'General Questions',
'categoryDescription' => '',
'questions' => [
['question' => 'What is this product?', 'answer' => '<p>Answer here...</p>'],
['question' => 'How do I get started?', 'answer' => '<p>Answer here...</p>'],
],
],
[
'categoryName' => 'Billing',
'categoryDescription' => '',
'questions' => [
['question' => 'What payment methods do you accept?', 'answer' => '<p>Answer here...</p>'],
],
],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'faq-categories',
]);
?>
<div <?php echo $wrapper_attributes; ?>>
<div class="faq-categories__list" data-proto-repeater="categories">
<?php foreach ($categories as $category) : ?>
<section class="faq-category" data-proto-repeater-item>
<header class="faq-category__header">
<h2 class="faq-category__name" data-proto-field="categoryName">
<?php echo esc_html($category['categoryName'] ?? ''); ?>
</h2>
<?php if (!empty($category['categoryDescription'])) : ?>
<div class="faq-category__description" data-proto-field="categoryDescription">
<?php echo wp_kses_post($category['categoryDescription']); ?>
</div>
<?php endif; ?>
</header>
<dl class="faq-category__questions" data-proto-repeater="questions">
<?php foreach ($category['questions'] ?? [] as $qa) : ?>
<div class="faq-item" data-proto-repeater-item>
<dt class="faq-item__question" data-proto-field="question">
<?php echo esc_html($qa['question'] ?? ''); ?>
</dt>
<dd class="faq-item__answer" data-proto-field="answer">
<?php echo wp_kses_post($qa['answer'] ?? ''); ?>
</dd>
</div>
<?php endforeach; ?>
</dl>
</section>
<?php endforeach; ?>
</div>
</div>
\`\`\`
---
### Example 6: Multi-Level Menu
A three-level navigation menu structure.
**block.json:**
\`\`\`json
{
"name": "proto-blocks/mega-menu",
"title": "Mega Menu",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"menuItems": {
"type": "repeater",
"itemLabel": "label",
"fields": {
"label": { "type": "text", "tagName": "span" },
"link": { "type": "link" },
"children": {
"type": "repeater",
"itemLabel": "label",
"fields": {
"label": { "type": "text", "tagName": "span" },
"link": { "type": "link" },
"grandchildren": {
"type": "repeater",
"itemLabel": "label",
"fields": {
"label": { "type": "text", "tagName": "span" },
"link": { "type": "link" }
}
}
}
}
}
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$menu_items = $attributes['menuItems'] ?? [];
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'mega-menu',
]);
// Recursive function to render menu items
function render_menu_item($item, $level = 0) {
$link = $item['link'] ?? null;
$children = $item['children'] ?? $item['grandchildren'] ?? [];
$has_children = !empty($children);
$field_names = ['children', 'grandchildren'];
$repeater_name = isset($item['grandchildren']) ? 'grandchildren' : 'children';
?>
<li class="mega-menu__item mega-menu__item--level-<?php echo $level; ?>" data-proto-repeater-item>
<a
href="<?php echo esc_url($link['url'] ?? '#'); ?>"
class="mega-menu__link"
data-proto-field="link"
>
<span data-proto-field="label"><?php echo esc_html($item['label'] ?? ''); ?></span>
</a>
<?php if ($has_children && $level < 2) : ?>
<ul class="mega-menu__submenu" data-proto-repeater="<?php echo $repeater_name; ?>">
<?php foreach ($children as $child) : ?>
<?php render_menu_item($child, $level + 1); ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php
}
?>
<nav <?php echo $wrapper_attributes; ?>>
<ul class="mega-menu__list" data-proto-repeater="menuItems">
<?php foreach ($menu_items as $item) : ?>
<?php render_menu_item($item, 0); ?>
<?php endforeach; ?>
</ul>
</nav>
\`\`\`
---
## Repeaters with Complex Fields
### Example 7: Team Grid with All Field Types
**block.json:**
\`\`\`json
{
"name": "proto-blocks/team-grid",
"title": "Team Grid",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"members": {
"type": "repeater",
"min": 1,
"max": 20,
"itemLabel": "name",
"collapsible": true,
"fields": {
"photo": {
"type": "image",
"sizes": ["medium", "large"]
},
"name": {
"type": "text",
"tagName": "h3"
},
"role": {
"type": "text",
"tagName": "span"
},
"bio": {
"type": "wysiwyg"
},
"email": {
"type": "link"
},
"socialLinks": {
"type": "repeater",
"max": 5,
"itemLabel": "platform",
"fields": {
"platform": {
"type": "text",
"tagName": "span"
},
"url": {
"type": "link"
}
}
}
}
}
},
"controls": {
"columns": {
"type": "range",
"label": "Columns",
"min": 2,
"max": 5,
"default": 4
},
"cardStyle": {
"type": "select",
"label": "Card Style",
"default": "standard",
"options": [
{ "key": "standard", "label": "Standard" },
{ "key": "minimal", "label": "Minimal" },
{ "key": "bordered", "label": "Bordered" }
]
},
"showBio": {
"type": "toggle",
"label": "Show Bio",
"default": true
},
"showSocial": {
"type": "toggle",
"label": "Show Social Links",
"default": true
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$members = $attributes['members'] ?? [];
$columns = $attributes['columns'] ?? 4;
$card_style = $attributes['cardStyle'] ?? 'standard';
$show_bio = $attributes['showBio'] ?? true;
$show_social = $attributes['showSocial'] ?? true;
// Default members for editor
if (empty($members)) {
$members = [
[
'photo' => null,
'name' => 'Team Member',
'role' => 'Position',
'bio' => '',
'email' => null,
'socialLinks' => [],
],
];
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => "team-grid team-grid--{$card_style}",
'style' => "--columns: {$columns};",
]);
?>
<section <?php echo $wrapper_attributes; ?>>
<div class="team-grid__container" data-proto-repeater="members">
<?php foreach ($members as $member) : ?>
<article class="team-card" data-proto-repeater-item>
<div class="team-card__photo" data-proto-field="photo">
<?php if (!empty($member['photo']['url'])) : ?>
<img
src="<?php echo esc_url($member['photo']['url']); ?>"
alt="<?php echo esc_attr($member['photo']['alt'] ?? $member['name'] ?? ''); ?>"
/>
<?php else : ?>
<div class="team-card__photo-placeholder">
<span>👤</span>
</div>
<?php endif; ?>
</div>
<div class="team-card__content">
<h3 class="team-card__name" data-proto-field="name">
<?php echo esc_html($member['name'] ?? ''); ?>
</h3>
<span class="team-card__role" data-proto-field="role">
<?php echo esc_html($member['role'] ?? ''); ?>
</span>
<?php if ($show_bio) : ?>
<div class="team-card__bio" data-proto-field="bio">
<?php echo wp_kses_post($member['bio'] ?? ''); ?>
</div>
<?php endif; ?>
<?php
$email = $member['email'] ?? null;
if ($email && !empty($email['url'])) :
?>
<a
href="<?php echo esc_url($email['url']); ?>"
class="team-card__email"
data-proto-field="email"
>
<?php echo esc_html($email['text'] ?? 'Contact'); ?>
</a>
<?php endif; ?>
<?php if ($show_social && !empty($member['socialLinks'])) : ?>
<div class="team-card__social" data-proto-repeater="socialLinks">
<?php foreach ($member['socialLinks'] as $social) : ?>
<?php $social_link = $social['url'] ?? null; ?>
<a
href="<?php echo esc_url($social_link['url'] ?? '#'); ?>"
class="team-card__social-link"
data-proto-repeater-item
data-proto-field="url"
title="<?php echo esc_attr($social['platform'] ?? ''); ?>"
<?php echo !empty($social_link['target']) ? 'target="_blank" rel="noopener"' : ''; ?>
>
<span data-proto-field="platform">
<?php echo esc_html($social['platform'] ?? ''); ?>
</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
\`\`\`
---
## Styling Repeaters
### Grid Layouts
\`\`\`css
/* Dynamic columns with CSS custom property */
.repeater-grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 2rem;
}
/* Responsive override */
@media (max-width: 1024px) {
.repeater-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.repeater-grid {
grid-template-columns: 1fr;
}
}
\`\`\`
### Flexbox Lists
\`\`\`css
.repeater-flex {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.repeater-flex__item {
flex: 1 1 300px;
max-width: 100%;
}
\`\`\`
### Stack Layout
\`\`\`css
.repeater-stack {
display: flex;
flex-direction: column;
gap: 1rem;
}
\`\`\`
### Masonry (CSS Grid)
\`\`\`css
.repeater-masonry {
columns: var(--columns, 3);
column-gap: 1.5rem;
}
.repeater-masonry__item {
break-inside: avoid;
margin-bottom: 1.5rem;
}
\`\`\`
### Empty State Styling
\`\`\`css
/* Style empty repeater items for editor */
.repeater-item__title:empty::before {
content: 'Click to add title';
color: #94a3b8;
font-style: italic;
}
.repeater-item__image--empty {
min-height: 150px;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #cbd5e1;
cursor: pointer;
}
\`\`\`
---
## Repeaters with Interactivity
### Example 8: Interactive Accordion
Using the WordPress Interactivity API with repeaters.
**block.json:**
\`\`\`json
{
"name": "proto-blocks/interactive-accordion",
"title": "Interactive Accordion",
"supports": {
"interactivity": true
},
"viewScriptModule": "file:./view.js",
"protoBlocks": {
"version": "1.0",
"template": "template.php",
"fields": {
"items": {
"type": "repeater",
"min": 1,
"max": 20,
"itemLabel": "title",
"collapsible": true,
"fields": {
"title": {
"type": "text",
"tagName": "span"
},
"content": {
"type": "wysiwyg"
}
}
}
},
"controls": {
"allowMultiple": {
"type": "toggle",
"label": "Allow Multiple Open",
"default": false
},
"firstOpen": {
"type": "toggle",
"label": "First Item Open by Default",
"default": true
}
}
}
}
\`\`\`
**template.php:**
\`\`\`php
<?php
$items = $attributes['items'] ?? [];
$allow_multiple = $attributes['allowMultiple'] ?? false;
$first_open = $attributes['firstOpen'] ?? true;
// Default items for editor
if (empty($items)) {
$items = [
['title' => 'Section 1', 'content' => '<p>Content for section 1...</p>'],
['title' => 'Section 2', 'content' => '<p>Content for section 2...</p>'],
['title' => 'Section 3', 'content' => '<p>Content for section 3...</p>'],
];
}
// Interactivity context
$context = [
'allowMultiple' => $allow_multiple,
'openItems' => $first_open ? [0] : [],
];
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'interactive-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"
type="button"
data-wp-on--click="actions.toggle"
data-wp-bind--aria-expanded="state.isOpen"
>
<span class="accordion__title" 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__panel"
data-wp-bind--hidden="!state.isOpen"
>
<div class="accordion__content" 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 context = getContext();
return context.openItems.includes(context.index);
},
get icon() {
const context = getContext();
return context.openItems.includes(context.index) ? '−' : '+';
},
},
actions: {
toggle() {
const context = getContext();
const { index, allowMultiple, openItems } = context;
if (openItems.includes(index)) {
// Close this item
context.openItems = openItems.filter(i => i !== index);
} else {
// Open this item
if (allowMultiple) {
context.openItems = [...openItems, index];
} else {
context.openItems = [index];
}
}
},
},
});
\`\`\`
---
## Performance Considerations
### Limiting Items
Always set reasonable \`max\` limits:
\`\`\`json
{
"items": {
"type": "repeater",
"max": 20 // Prevent performance issues
}
}
\`\`\`
### Lazy Loading Images
\`\`\`php
<img
src="<?php echo esc_url($item['image']['url']); ?>"
alt="..."
loading="lazy"
decoding="async"
/>
\`\`\`
### Pagination for Large Lists
For very long lists, consider server-side pagination or "load more" patterns.
### Avoid Deep Nesting
Limit nesting to 2-3 levels maximum:
\`\`\`
✓ categories → questions (2 levels)
✓ sections → items → subitems (3 levels)
✗ level1 → level2 → level3 → level4 → level5 (too deep)
\`\`\`
---
## Common Patterns & Recipes
### Pattern: Alternating Layout
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<?php $is_even = $index % 2 === 0; ?>
<div class="item <?php echo $is_even ? 'item--left' : 'item--right'; ?>">
<!-- Content -->
</div>
<?php endforeach; ?>
\`\`\`
### Pattern: First Item Special
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<?php $is_first = $index === 0; ?>
<div class="item <?php echo $is_first ? 'item--featured' : ''; ?>">
<!-- Content -->
</div>
<?php endforeach; ?>
\`\`\`
### Pattern: Counter/Index Display
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<div class="step">
<span class="step__number"><?php echo $index + 1; ?></span>
<span data-proto-field="title"><?php echo esc_html($item['title']); ?></span>
</div>
<?php endforeach; ?>
\`\`\`
### Pattern: Last Item Different
\`\`\`php
<?php
$total = count($items);
foreach ($items as $index => $item) :
$is_last = $index === $total - 1;
?>
<div class="item <?php echo $is_last ? 'item--last' : ''; ?>">
<!-- Content -->
</div>
<?php endforeach; ?>
\`\`\`
---
## Troubleshooting Repeaters
### Issue: Items Not Editable
**Solution:** Ensure all three attributes are present:
\`\`\`php
<ul data-proto-repeater="items"> <!-- Container -->
<li data-proto-repeater-item> <!-- Item -->
<span data-proto-field="title"> <!-- Field -->
\`\`\`
### Issue: Can't Add/Remove Items
**Solution:** Check configuration:
\`\`\`json
{
"allowAdd": true,
"allowRemove": true
}
\`\`\`
Also verify you haven't hit \`max\` limit.
### Issue: Nested Repeater Not Working
**Solution:** Each level needs its own \`data-proto-repeater\` with correct field name:
\`\`\`php
<div data-proto-repeater="categories">
<div data-proto-repeater-item>
<div data-proto-repeater="questions"> <!-- Nested -->
<div data-proto-repeater-item>
<!-- Fields -->
</div>
</div>
</div>
</div>
\`\`\`
### Issue: Empty Repeater in Editor
**Solution:** Provide default items:
\`\`\`php
if (empty($items)) {
$items = [
['title' => 'Item 1'],
['title' => 'Item 2'],
];
}
\`\`\`
### Issue: Item Labels Not Showing
**Solution:** Use \`itemLabel\` with existing field name:
\`\`\`json
{
"itemLabel": "title", // Must match a field name
"fields": {
"title": { "type": "text" } // Field exists
}
}
\`\`\`
### Issue: Drag-Drop Not Working
**Solution:** Ensure \`allowReorder: true\` (default) and items have proper structure.
---
## Summary
Repeaters are essential for dynamic content in Proto-Blocks. Key points:
1. **Always use all three attributes**: \`data-proto-repeater\`, \`data-proto-repeater-item\`, \`data-proto-field\`
2. **Provide defaults** for empty editor state
3. **Use \`itemLabel\`** for meaningful editor labels
4. **Set reasonable limits** with \`min\` and \`max\`
5. **Enable \`collapsible\`** for complex items
6. **Limit nesting** to 2-3 levels
7. **Consider performance** for large lists
`;
}
export function getRepeaterDetails(topic) {
const topics = {
basics: getRepeaterBasics(),
nested: getNestedRepeaters(),
interactivity: getRepeaterInteractivity(),
patterns: getRepeaterPatterns(),
troubleshooting: getRepeaterTroubleshooting(),
};
return topics[topic] || `Unknown topic: ${topic}. Available: basics, nested, interactivity, patterns, troubleshooting`;
}
function getRepeaterBasics() {
return `# Repeater Basics
## Required Data Attributes
Every repeater needs three data attributes:
1. **Container**: \`data-proto-repeater="fieldName"\`
2. **Item wrapper**: \`data-proto-repeater-item\`
3. **Fields inside**: \`data-proto-field="nestedFieldName"\`
## Minimal Configuration
\`\`\`json
{
"items": {
"type": "repeater",
"fields": {
"text": { "type": "text" }
}
}
}
\`\`\`
## Minimal Template
\`\`\`php
<ul data-proto-repeater="items">
<?php foreach ($items as $item) : ?>
<li data-proto-repeater-item>
<span data-proto-field="text"><?php echo esc_html($item['text']); ?></span>
</li>
<?php endforeach; ?>
</ul>
\`\`\`
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| \`min\` | number | 0 | Minimum items |
| \`max\` | number | ∞ | Maximum items |
| \`itemLabel\` | string | — | Field for item labels |
| \`collapsible\` | boolean | false | Allow collapsing |
| \`allowAdd\` | boolean | true | Show add button |
| \`allowRemove\` | boolean | true | Show remove button |
| \`allowReorder\` | boolean | true | Enable drag-drop |
| \`allowDuplicate\` | boolean | true | Show duplicate button |
`;
}
function getNestedRepeaters() {
return `# Nested Repeaters
Repeaters can contain other repeaters for hierarchical data.
## Two-Level Example
\`\`\`json
{
"categories": {
"type": "repeater",
"itemLabel": "name",
"fields": {
"name": { "type": "text" },
"items": {
"type": "repeater",
"itemLabel": "title",
"fields": {
"title": { "type": "text" },
"description": { "type": "wysiwyg" }
}
}
}
}
}
\`\`\`
## Template Structure
\`\`\`php
<div data-proto-repeater="categories">
<?php foreach ($categories as $category) : ?>
<section data-proto-repeater-item>
<h2 data-proto-field="name"><?php echo esc_html($category['name']); ?></h2>
<ul data-proto-repeater="items">
<?php foreach ($category['items'] ?? [] as $item) : ?>
<li data-proto-repeater-item>
<h3 data-proto-field="title"><?php echo esc_html($item['title']); ?></h3>
<div data-proto-field="description"><?php echo wp_kses_post($item['description']); ?></div>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endforeach; ?>
</div>
\`\`\`
## Best Practices
1. Limit to 2-3 nesting levels
2. Use meaningful \`itemLabel\` at each level
3. Provide comprehensive defaults
4. Consider performance implications
`;
}
function getRepeaterInteractivity() {
return `# Repeaters with Interactivity API
## Setup
\`\`\`json
{
"supports": { "interactivity": true },
"viewScriptModule": "file:./view.js"
}
\`\`\`
## Template with Context
\`\`\`php
<?php
$context = [
'openItems' => [],
'allowMultiple' => false,
];
?>
<div
data-wp-interactive="proto-blocks/accordion"
data-wp-context='<?php echo wp_json_encode($context); ?>'
data-proto-repeater="items"
>
<?php foreach ($items as $index => $item) : ?>
<div
data-proto-repeater-item
data-wp-context='{"index": <?php echo $index; ?>}'
data-wp-class--is-open="state.isOpen"
>
<button data-wp-on--click="actions.toggle">
<span data-proto-field="title"><?php echo esc_html($item['title']); ?></span>
</button>
<div data-wp-bind--hidden="!state.isOpen">
<div data-proto-field="content"><?php echo wp_kses_post($item['content']); ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
\`\`\`
## JavaScript Store
\`\`\`javascript
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];
}
}
}
});
\`\`\`
`;
}
function getRepeaterPatterns() {
return `# Common Repeater Patterns
## Grid Layout
\`\`\`php
<div class="grid" style="--columns: <?php echo $columns; ?>;" data-proto-repeater="items">
<?php foreach ($items as $item) : ?>
<div class="grid__item" data-proto-repeater-item>
<!-- Content -->
</div>
<?php endforeach; ?>
</div>
\`\`\`
\`\`\`css
.grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 2rem;
}
\`\`\`
## Alternating Layout
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<div class="item <?php echo $index % 2 === 0 ? 'item--left' : 'item--right'; ?>">
<!-- Content -->
</div>
<?php endforeach; ?>
\`\`\`
## Numbered Steps
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<div class="step" data-proto-repeater-item>
<span class="step__number"><?php echo $index + 1; ?></span>
<span data-proto-field="title"><?php echo esc_html($item['title']); ?></span>
</div>
<?php endforeach; ?>
\`\`\`
## Featured First Item
\`\`\`php
<?php foreach ($items as $index => $item) : ?>
<div class="item <?php echo $index === 0 ? 'item--featured' : ''; ?>">
<!-- Content -->
</div>
<?php endforeach; ?>
\`\`\`
## Empty State Defaults
\`\`\`php
<?php
$items = $attributes['items'] ?? [];
if (empty($items)) {
$items = [
['title' => 'Item 1', 'content' => ''],
['title' => 'Item 2', 'content' => ''],
['title' => 'Item 3', 'content' => ''],
];
}
?>
\`\`\`
`;
}
function getRepeaterTroubleshooting() {
return `# Repeater Troubleshooting
## Items Not Editable
**Problem:** Can't click to edit repeater items.
**Solution:** Ensure all three required attributes:
\`\`\`php
<ul data-proto-repeater="items">
<li data-proto-repeater-item>
<span data-proto-field="title">...</span>
</li>
</ul>
\`\`\`
## Can't Add Items
**Problem:** Add button doesn't appear or doesn't work.
**Solutions:**
1. Check \`allowAdd: true\` in config
2. Verify you haven't hit \`max\` limit
3. Ensure valid JSON in block.json
## Nested Repeater Issues
**Problem:** Inner repeater not working.
**Solution:** Each level needs correct attribute:
\`\`\`php
<div data-proto-repeater="outer">
<div data-proto-repeater-item>
<div data-proto-repeater="inner">
<div data-proto-repeater-item>
<span data-proto-field="text">...</span>
</div>
</div>
</div>
</div>
\`\`\`
## Empty in Editor
**Problem:** No items visible when block first added.
**Solution:** Provide default items:
\`\`\`php
if (empty($items)) {
$items = [['title' => 'Default']];
}
\`\`\`
## Item Labels Not Showing
**Problem:** Items show "Item 1", "Item 2" instead of field values.
**Solution:** \`itemLabel\` must match a field name:
\`\`\`json
{
"itemLabel": "title",
"fields": {
"title": { "type": "text" }
}
}
\`\`\`
## Drag-Drop Not Working
**Problem:** Can't reorder items.
**Solutions:**
1. Ensure \`allowReorder: true\` (default)
2. Check for CSS conflicts
3. Verify proper item structure
## Performance Issues
**Problem:** Editor slow with many items.
**Solutions:**
1. Set \`max\` limit
2. Enable \`collapsible: true\`
3. Reduce nested complexity
`;
}