This file documents patterns for integrating React with PAD templates.
When integrating React with PAD, the key principle is separation of concerns:
Use this pattern for data stored in JSON files in _data/.
_data/nav.json:
[
{ "page": "index", "label": "Home", "icon": "🏠" },
{ "page": "about", "label": "About", "icon": "📖" }
]
_inits.pad:
<nav>
{local:nav.json}
<a href="?{$page}" {if $padPage == $page}class="active"{/if}>
{$icon} {$label}
</a>
{/local:nav.json}
</nav>
_tags/json.php:
<?php
// Read JSON file from _data/, compact and HTML-escape for attributes
$jsonContent = file_get_contents(APP . "_data/$padParm.json");
$jsonData = json_decode($jsonContent, true);
$jsonCompact = json_encode($jsonData);
$padContent = htmlspecialchars($jsonCompact, ENT_QUOTES, 'UTF-8');
return TRUE;
?>
template.pad:
<div id="app" data-products="{json 'products' | ignore}"></div>
{ignore}<script>
const products = JSON.parse(document.getElementById('app').dataset.products);
// React can now use products
</script>{/ignore}
Use this pattern for database-driven or dynamic data.
apps/myapp/
├── _providers/ # PHP data providers
│ ├── topic.php # Returns topic record
│ ├── user.php # Returns user record
│ └── posts.php # Returns posts array
└── _tags/
└── reactData.php # Custom tag implementation
_tags/reactData.php:
<?php
// Get tag parameters
$padId = padTagParm('id', ''); // HTML element ID
$padProvider = padTagParm('provider', ''); // Provider name
// Execute provider to get data
$padProviderFile = APP . "_providers/$padProvider.php";
if (file_exists($padProviderFile)) {
$padData = include $padProviderFile;
} else {
padError("Provider not found: $padProvider");
}
// Convert to JSON and HTML-escape for attribute
$padJson = json_encode($padData);
$padJsonEscaped = htmlspecialchars($padJson, ENT_QUOTES, 'UTF-8');
// Generate HTML div with data attribute
$padContent = "<div id=\"$padId\" data=\"$padJsonEscaped\"></div>";
return TRUE;
?>
_providers/topic.php (single record):
<?php
// Providers return data - they have access to all variables from the page
return db("RECORD * FROM forum_topics WHERE id={0}", [$id]);
?>
_providers/posts.php (array):
<?php
// IMPORTANT: Use array_values() to ensure proper JSON array (not object with numeric keys)
$posts = db("ARRAY * FROM forum_posts WHERE topic_id={0}", [$id]);
return array_values($posts);
?>
topic.pad:
<h1>Forum Topic</h1>
<!-- Multiple data sources - each with unique ID -->
{reactData id='topic', provider='topic', $id=$id}
{reactData id='board', provider='board', $boardId=$boardId}
{reactData id='user', provider='user', $userId=$userId}
{reactData id='posts', provider='posts', $id=$id}
<div id="react-app"></div>
<script type="text/babel" src="/react/topic/display.js"></script>
// ❌ WRONG - dataset only works for data-* attributes, returns undefined for plain "data"
const topicElem = document.getElementById('topic');
const topic = JSON.parse(topicElem.dataset.data); // FAILS!
// ✅ CORRECT - Use getAttribute() for plain "data" attribute
const topicElem = document.getElementById('topic');
const topic = JSON.parse(topicElem.getAttribute('data')); // WORKS!
www/react/topic/display.js:
function TopicDisplay() {
// Get data from all reactData divs
// IMPORTANT: Use getAttribute('data') NOT dataset.data!
const topicElem = document.getElementById('topic');
const boardElem = document.getElementById('board');
const userElem = document.getElementById('user');
const postsElem = document.getElementById('posts');
const topic = JSON.parse(topicElem.getAttribute('data'));
const board = JSON.parse(boardElem.getAttribute('data'));
const user = JSON.parse(userElem.getAttribute('data'));
const posts = JSON.parse(postsElem.getAttribute('data'));
return (
<div className="topic-display">
<div className="breadcrumb">
<a href="?forum/index">Forum</a> →
<a href={`?forum/board&id=${board.id}`}>{board.name}</a>
</div>
<h1>{topic.title}</h1>
<div className="topic-meta">
Posted by {user.username} on {new Date(topic.created_at).toLocaleDateString()}
</div>
<div className="posts">
{posts.map((post, index) => (
<div key={post.id} className="post">
<div className="post-header">
Post #{index + 1} by {post.username}
</div>
<div className="post-content">
{post.content}
</div>
</div>
))}
</div>
</div>
);
}
// Render the component
const root = ReactDOM.createRoot(document.getElementById('react-app'));
root.render(<TopicDisplay />);
| Principle | Description |
|---|---|
| Server-side data | Use _data/*.json files (static) or _providers/*.php (dynamic) |
| PAD responsibility | Data preparation and HTML structure |
| React responsibility | Client-side interactivity |
| JSON in attributes | Use \| ignore pipe with {json} tag |
| JavaScript blocks | Wrap in {ignore}...{/ignore} tags |
| Data attribute access | Use getAttribute('data') NOT dataset.data |
| JSON arrays | Use array_values() in providers for proper arrays |
| Unique IDs | Each {reactData} needs unique id parameter |
| Variable access | Providers have access to all page variables |
PAD parses { } as tags. When working with JavaScript/React:
{ignore}
<script>
const user = { name: 'Alice', role: 'Developer' };
if (user.active) { console.log('Active'); }
</script>
{/ignore}
<script src="/js/app.js"></script>
<script type="text/babel" src="/react/components/MyComponent.js"></script>
<div data-config="{echo $configJson | ignore}"></div>
Same principle applies - prefer external CSS files:
<link rel="stylesheet" href="/css/react-components.css">
Or use {ignore} for inline styles:
{ignore}
<style>
.topic-display { padding: 20px; }
.post { margin: 10px 0; }
</style>
{/ignore}
Recommended structure for React integration:
apps/myapp/
├── _data/ # Static JSON data
│ ├── nav.json
│ └── config.json
├── _providers/ # Dynamic data providers
│ ├── user.php
│ └── posts.php
├── _tags/
│ ├── json.php # {json} tag for static data
│ └── reactData.php # {reactData} tag for dynamic data
└── pages/
└── forum/
└── topic.pad
www/
├── react/ # React components
│ └── topic/
│ └── display.js
├── js/ # Plain JavaScript
└── css/ # Stylesheets
function MyComponent() {
const dataElem = document.getElementById('my-data');
if (!dataElem) {
return <div>Loading...</div>;
}
const data = JSON.parse(dataElem.getAttribute('data'));
return <div>{/* render data */}</div>;
}
function MyComponent() {
const dataElem = document.getElementById('my-data');
try {
const data = JSON.parse(dataElem.getAttribute('data'));
return <div>{data.title}</div>;
} catch (e) {
return <div>Error loading data</div>;
}
}
{reactData id='users', provider='users'}
{reactData id='roles', provider='roles'}
{reactData id='permissions', provider='permissions'}
<div id="admin-panel"></div>
function AdminPanel() {
const users = JSON.parse(document.getElementById('users').getAttribute('data'));
const roles = JSON.parse(document.getElementById('roles').getAttribute('data'));
const permissions = JSON.parse(document.getElementById('permissions').getAttribute('data'));
// Combine data as needed
return <div>{/* admin UI */}</div>;
}