Reference - Output types - web


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&amp;...&amp;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>