App: react - Page: examples/cart
PHP
examples/cart.php
|
HTML
examples/cart.pad
|
Result
|
<?php
// The cart is the session's: product id => quantity. A post adds one (op=add), takes one // off (op=remove) or empties it (op=clear), and the answer to it - like the page itself - // is the cart as the islands show it: lines, count and total. Stock is the server's to // check: a product sold out cannot be added, however the button was pressed.
$products = array_column ( reactData ( 'products' ), NULL, 'id' ); $counts = padSession ( 'reactCart', [] );
if ( padRequestIs ( 'POST' ) ) {
$op = padRequest ( 'op', '' ); $id = (int) padRequest ( 'id', 0 );
if ( $op == 'clear' ) $counts = []; elseif ( $op == 'add' and isset ( $products [$id] ) and $products [$id] ['stock'] > ( $counts [$id] ?? 0 ) ) $counts [$id] = ( $counts [$id] ?? 0 ) + 1; elseif ( $op == 'remove' and isset ( $counts [$id] ) and --$counts [$id] <= 0 ) unset ( $counts [$id] );
padSessionPut ( 'reactCart', $counts );
}
$cart = reactCart ( $counts ); $shelf = [ 'products' => array_slice ( array_values ( $products ), 0, 9 ), 'cart' => $cart ];
$padExpose = [ 'cart' ];
?>
|
<div class="cols-2" style="grid-template-columns: minmax(0, 1.6fr) minmax(260px, 1fr)"> <div data-island="Shelf" data-props="{^shelf}"></div> <aside data-island="Basket" data-props="{^cart}"></aside> </div>
<script type="text/babel" src="{$padRoot}react/examples/cart.js"></script>
|
|
App: react - Page: examples/chart
PHP
examples/chart.php
|
HTML
examples/chart.pad
|
Result
|
<?php
// The chart the request asks for: its kind, how many months, which channels. The browser // gets the whole page; the component asks for &padFragment=chart with its choices and gets // the {fragment 'chart'} of chart.pad alone - the SVG PAD drew, nothing around it.
$kind = padRequest ( 'kind', 'bar' ); $kind = in_array ( $kind, [ 'bar', 'stacked', 'line', 'hbar' ] ) ? $kind : 'bar'; $months = max ( 3, min ( 12, (int) padRequest ( 'months', 12 ) ) ); $channels = array_values ( array_intersect ( [ 'online', 'shop', 'wholesale' ], explode ( ',', reactText ( 'channels', 'online,shop,wholesale' ) ) ) );
if ( ! $channels ) $channels = [ 'online' ];
$sales = array_slice ( reactData ( 'sales' ), -$months ); $value = implode ( ', ', $channels ); $stacked = ( $kind == 'stacked' ); $chartKind = $stacked ? 'bar' : $kind; $total = array_sum ( array_map ( fn ( $row ) => array_sum ( array_intersect_key ( $row, array_flip ( $channels ) ) ), $sales ) );
$choice = [ 'kind' => $kind, 'months' => $months, 'channels' => $channels ];
?>
|
<div data-island="ChartStudio" data-props="{^choice}"> {fragment 'chart'} <div class="chart-frame" data-total="{$total}"> {if $stacked} {chart 'bar', data='sales', label='month', value=$value, stacked, title='Sales per month, in thousands'} {else} {chart $chartKind, data='sales', label='month', value=$value, title='Sales per month, in thousands'} {/if} </div> {/fragment} </div>
<script type="text/babel" src="{$padRoot}react/examples/chart.js"></script>
|
|
App: react - Page: examples/click
PHP
|
HTML
examples/click.pad
|
Result
|
|
<div id="example1"></div>
<script type="text/babel" src="{$padRoot}react/examples/click.js"></script>
|
|
App: react - Page: examples/components
PHP
|
HTML
examples/components.pad
|
Result
|
|
<div id="components-demo"></div>
<script type="text/babel" src="{$padRoot}react/examples/components.js"></script>
|
|
App: react - Page: examples/counter
PHP
examples/counter.php
|
HTML
examples/counter.pad
|
Result
|
<?php
// The start value comes from the server; everything after it is React state.
$initialCount = 10;
?>
|
<div id="counter-app" data-initial-count="{$initialCount}"></div>
<script type="text/babel" src="{$padRoot}react/examples/counter.js"></script>
|
|
App: react - Page: examples/enhance
PHP
examples/enhance.php
|
HTML
examples/enhance.pad
|
Result
|
<?php
$people = reactData ( 'team' );
?>
|
<div data-island="TeamTable" data-props="{^people}"> <table class="table"> <thead> <tr><th>Name</th><th>Role</th><th>Team</th><th>City</th><th class="num">Commits</th></tr> </thead> <tbody> {people} <tr><td>{$name}</td><td>{$role}</td><td>{$team}</td><td>{$city}</td><td class="num">{$commits}</td></tr> {/people} </tbody> </table> </div>
<script type="text/babel" src="{$padRoot}react/examples/enhance.js"></script>
|
|
App: react - Page: examples/feedback
PHP
examples/feedback.php
|
HTML
examples/feedback.pad
|
Result
|
<?php
// The wall is this visitor's own, kept in the session: padSession reads it, padSessionPut // writes it. A post comes from the component's fetch - with $padCsrf on in the application's // configuration it carries the session's token in an X-CSRF-Token header, or PAD answers 403 // before this file runs. padValidate answers a message per field that broke its rules; // with none the message goes on the wall. The answer to the post is the JSON of $padExpose. // // One list of rules for both sides: padValidate checks the post with it, and the component // gets it as padValidateClient exports it, for the browser's checker that {validator} in // the template brings - the same rules, the same messages, before the post leaves.
$rules = [ 'name' => 'required|max:40', 'email' => 'required|email', 'mood' => 'required|in:happy,curious,puzzled', 'message' => 'required|min:10|max:280' ];
$wall = padSession ( 'reactWall', [] ); $errors = []; $saved = FALSE;
if ( padRequestIs ( 'POST' ) ) {
$errors = padValidate ( $rules );
if ( ! $errors ) {
array_unshift ( $wall, [ 'name' => trim ( padRequest ( 'name' ) ), 'mood' => padRequest ( 'mood' ), 'message' => trim ( padRequest ( 'message' ) ), 'at' => date ( 'H:i' ) ] );
$wall = array_slice ( $wall, 0, 12 ); $saved = padSessionPut ( 'reactWall', $wall );
}
}
$start = [ 'wall' => $wall, 'rules' => padValidateClient ( $rules ) ]; $padExpose = [ 'saved', 'errors', 'wall' ];
?>
|
<div data-island="FeedbackWall" data-props="{^start}"> <p class="island-fallback">The form needs JavaScript - it posts with fetch().</p> </div>
{validator} <script type="text/babel" src="{$padRoot}react/examples/feedback.js"></script>
|
|
App: react - Page: examples/form
PHP
|
HTML
examples/form.pad
|
Result
|
|
<div id="example2"></div>
<script type="text/babel" src="{$padRoot}react/examples/form.js"></script>
|
|
App: react - Page: examples/live
PHP
examples/live.php
|
HTML
examples/live.pad
|
Result
|
<?php
// A small status board. The component asks for it every few seconds with padFormat=json; // padCacheIncrement keeps a count of the asks of every visitor together, under // DATA/cache/app/react/ - under a lock, so asks that come at once are each counted. The // rest is what the server knows at that moment.
$hits = (int) padCacheIncrement ( 'liveHits', 1, 86400 );
$load = function_exists ( 'sys_getloadavg' ) ? sys_getloadavg () : FALSE;
$status = [ 'time' => date ( 'H:i:s' ), 'date' => date ( 'l j F Y' ), 'hits' => $hits, 'php' => PHP_VERSION, 'memory' => round ( memory_get_peak_usage () / 1048576, 1 ), 'load' => $load ? round ( $load [0], 2 ) : NULL, 'pid' => getmypid () ];
$settings = [ 'every' => 2 ]; $padExpose = [ 'status' ];
?>
|
<div data-island="LiveBoard" data-props="{^settings}"> <p class="island-fallback">The board asks the server for its numbers as soon as it runs.</p> </div>
<script type="text/babel" src="{$padRoot}react/examples/live.js"></script>
|
|
App: react - Page: examples/products
PHP
|
HTML
examples/products.pad
|
Result
|
|
<div id="products" data-products="{json 'products' | ignore}"></div>
<script type="text/babel" src="{$padRoot}react/examples/products.js"></script>
|
|
App: react - Page: examples/props
PHP
examples/props.php
|
HTML
examples/props.pad
|
Result
|
<?php
// What the islands start with. Each member's row carries its card - the props of one // island - and the stats are one more island's; the template writes them with the ^ // sigil, as JSON escaped for the attribute.
$team = reactData ( 'team' ); $most = max ( array_column ( $team, 'commits' ) );
$members = [];
foreach ( $team as $member ) $members [] = [ 'name' => $member ['name'], 'role' => $member ['role'], 'card' => $member + [ 'most' => $most ] ];
$stats = [ 'people' => count ( $team ), 'commits' => array_sum ( array_column ( $team, 'commits' ) ), 'cities' => count ( array_unique ( array_column ( $team, 'city' ) ) ), 'teams' => count ( array_unique ( array_column ( $team, 'team' ) ) ) ];
?>
|
<div data-island="TeamStats" data-props="{^stats}"> <p class="island-fallback">{$stats.people} people, {$stats.commits} commits</p> </div>
<div class="grid" style="margin-top: 22px"> {members} <div data-island="MemberCard" data-props="{^card}"> <div class="card"><strong>{$name}</strong><br><span class="muted">{$role}</span></div> </div> {/members} </div>
<script type="text/babel" src="{$padRoot}react/examples/props.js"></script>
|
|
App: react - Page: examples/search
PHP
examples/search.php
|
HTML
examples/search.pad
|
Result
|
<?php
// One page, two answers. The browser asking ?examples/search gets search.pad with the // first result in it; the component asks the same page with padFormat=json and gets // $result as JSON - the templates never run for that request. $padExpose is the list of // what may leave the server: nothing else of this page does.
$query = trim ( reactText ( 'q' ) ); $category = reactText ( 'category' ); $order = reactText ( 'sort', 'name' ); $perPage = 6;
$products = reactData ( 'products' ); $categories = array_values ( array_unique ( array_column ( $products, 'category' ) ) );
$found = array_values ( array_filter ( $products, fn ( $product ) => ( $category === '' or $product ['category'] === $category ) and ( $query === '' or stripos ( $product ['name'] . ' ' . $product ['blurb'], $query ) !== FALSE ) ) );
usort ( $found, match ( $order ) { 'cheap' => fn ( $a, $b ) => $a ['price'] <=> $b ['price'], 'dear' => fn ( $a, $b ) => $b ['price'] <=> $a ['price'], 'rating' => fn ( $a, $b ) => $b ['rating'] <=> $a ['rating'], default => fn ( $a, $b ) => strcasecmp ( $a ['name'], $b ['name'] ) } );
$pages = max ( 1, (int) ceil ( count ( $found ) / $perPage ) ); $pageNo = min ( $pages, max ( 1, (int) padRequest ( 'p', 1 ) ) );
$result = [ 'items' => array_slice ( $found, ( $pageNo - 1 ) * $perPage, $perPage ), 'total' => count ( $found ), 'page' => $pageNo, 'pages' => $pages ];
$start = [ 'result' => $result, 'categories' => $categories ]; $items = $result ['items'];
$padExpose = [ 'result' ];
?>
|
<div data-island="CatalogSearch" data-props="{^start}"> <ul> {items}<li>{$emoji} {$name} - {$price}</li>{/items} </ul> </div>
<script type="text/babel" src="{$padRoot}react/examples/search.js"></script>
|
|
App: react - Page: examples/toggle
PHP
|
HTML
examples/toggle.pad
|
Result
|
|
<div id="example4"></div>
<script type="text/babel" src="{$padRoot}react/examples/toggle.js"></script>
|
|
App: react - Page: examples/topic
PHP
|
HTML
examples/topic.pad
|
Result
|
|
{reactData id='topic', provider='topic', $id=1} {reactData id='board', provider='board_from_topic'} {reactData id='user', provider='user_from_topic'} {reactData id='posts', provider='posts_from_topic'}
<div id="topic-display"></div> <script type="text/babel" src="{$padRoot}react/examples/topic.js"></script>
|
|
App: react - Page: guide
PHP
|
HTML
guide.pad
|
Result
|
|
{meta title='Guide - PAD + React'} <header class="example-head"> <h1>Build your own island</h1> <p class="lede"> Four files make a page with React in it: the data, the template, the component - and the runtime every page already has. Then the same page can answer the component's questions. </p> </header>
<div class="cols-2">
<div> <h2>1. The data, in PHP</h2> <p>The page's PHP gathers what the component starts with - from the database, a file in <code>_data/</code>, the session. Nothing about React here.</p> {highlight 'php', file='_guide/orders.php'} </div>
<div> <h2>2. The island, in the template</h2> <p>An element with <code>data-island</code> names the component; <code>{^orders}</code> writes the field as JSON, escaped for the attribute. What stands inside shows until React takes over.</p> {highlight 'pad', file='_guide/orders.pad'} </div>
</div>
<h2>3. The component, in www/</h2> <p>A plain file under <code>www/react/</code>, served as it is - so its braces never meet the PAD parser. <code>PadReact.island</code> mounts it on every island of that name, the JSON as its props.</p> {highlight 'js', file='_guide/orders.js'}
<h2>4. Talking back</h2> <div class="cols-2"> <div> <p>The page answers JSON when asked - but only what <code>$padExpose</code> names. The templates do not run for such a request. A post needs the session's CSRF token, which <code>PadReact.post</code> sends in a header when <code>$padCsrf</code> is on.</p> {highlight 'php'} // orders.php - one more line $padExpose = [ 'orders' ];
// a post from the component if ( padRequestIs ( 'POST' ) ) { $errors = padValidate ( [ 'qty' => 'required|integer|min:1' ] ); if ( ! $errors ) db ( "UPDATE orders SET qty={0} WHERE id={1}", [ $qty, $id ] ); } {/highlight} </div> <div> <p>From the component it is one call each - and <code>{fragment 'name'}</code> in the template lets a component fetch one rendered part of a page as HTML.</p> {highlight 'js'} // ?orders&padFormat=json const { orders } = await PadReact.get('orders', { status: 'open' });
// POST ?orders&padFormat=json, X-CSRF-Token: ... const answer = await PadReact.post('orders', { id: 7, qty: 2 });
// ?orders&padFragment=summary - HTML const html = await PadReact.fragment('orders', 'summary');
// as a hook: asked again whenever status changes const { data, loading } = PadReact.useData('orders', { status }, first); {/highlight} </div> </div>
<h2>The runtime</h2> <p><code>www/react/pad-react.js</code> - plain JavaScript, loaded by <code>_inits.pad</code> on every page.</p> <table class="table"> <thead><tr><th>Call</th><th>What it does</th></tr></thead> <tbody> <tr><td><code>PadReact.island(name, Component)</code></td><td>Renders <code>Component</code> on every <code>[data-island=name]</code>, with the JSON of <code>data-props</code> as props (a list arrives as <code>props.data</code>) and what PAD rendered inside as <code>props.serverHtml</code>.</td></tr> <tr><td><code>PadReact.get(page, params)</code></td><td>The page's exposed data: <code>?page&...&padFormat=json</code>, parsed.</td></tr> <tr><td><code>PadReact.post(page, body, { token })</code></td><td>Posts the body form-encoded with the <code>X-CSRF-Token</code> header from <code><meta name="csrf-token"></code>; <code>token: false</code> leaves it out.</td></tr> <tr><td><code>PadReact.fragment(page, name, params)</code></td><td>One <code>{fragment}</code> of the page as HTML.</td></tr> <tr><td><code>PadReact.useData(page, params, first)</code></td><td>A hook: <code>{ data, loading, error }</code>, asked again when the params change, the request before aborted.</td></tr> <tr><td><code>PadReact.useDebounced(value, ms)</code></td><td>The value once it stood still for <code>ms</code> - for a search box.</td></tr> </tbody> </table> <p class="small muted">A request that does not succeed throws an error with <code>status</code> and the parsed <code>body</code>. Every request is told to the page as a <code>pad:request</code> event - the request log under the examples listens to it.</p>
<h2>Good to know</h2> <div class="grid"> <div class="card"> <h3>Keep JSX out of templates</h3> <p>PAD reads every <code>{ }</code> of a template. Components live in <code>www/</code>, loaded with <code><script type="text/babel" src></code>; for a short inline script there is <code>{ignore}</code>.</p> </div> <div class="card"> <h3>Values are text</h3> <p>A field in <code>data-props</code> is escaped for the attribute and never runs as PAD, whatever a visitor typed into it - React escapes it once more when it renders.</p> </div> <div class="card"> <h3>Server first</h3> <p>Give the island its first data in the props and put real HTML inside it: the page shows something before any script ran, and search engines see it.</p> </div> <div class="card"> <h3>Going to production</h3> <p>Babel in the browser is for learning. Compile the components with esbuild or Vite into one file, load the production React builds, and drop <code>type="text/babel"</code> - the PAD side stays as it is.</p> </div> </div>
|
|
|