PAD + React

Composition

Small components - a card, a button, a todo item - put together into bigger ones with props and children.

propschildren
Live

How it is built

PAD, on the server React, in the browser

<div id="components-demo"></div>

<script type="text/babel" src="{$padRoot}react/examples/components.js"></script>
// Composition: small components that take props and children, put together into bigger
// ones. Card and Button are used several times with different props; TodoApp is built from
// TodoItem - the list's state lives in the parent, the items only report what happened.

function Card({ title, tone = 'react', children }) {
  return (
    <div className="card" style={{ borderTop: `3px solid var(--${tone})` }}>
      {title && <h3 style={{ marginTop: 0, color: `var(--${tone})` }}>{title}</h3>}
      {children}
    </div>
  );
}

function Button({ variant = '', onClick, children }) {
  return <button className={'btn-sm ' + variant} onClick={onClick}>{children}</button>;
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className="rise" style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', borderBottom: '1px solid var(--line)' }}>
      <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} aria-label={todo.text} />
      <span style={{ flex: 1, textDecoration: todo.done ? 'line-through' : 'none', color: todo.done ? 'var(--faint)' : 'inherit' }}>{todo.text}</span>
      <button className="btn-icon btn-ghost" onClick={() => onDelete(todo.id)} aria-label={'Delete ' + todo.text}>×</button>
    </li>
  );
}

function TodoApp() {
  const [todos, setTodos] = React.useState([
    { id: 1, text: 'Learn React basics', done: true },
    { id: 2, text: 'Build a component', done: false },
    { id: 3, text: 'Mount it on a PAD page', done: false }
  ]);
  const [text, setText] = React.useState('');

  const add = event => {
    event.preventDefault();
    if (!text.trim()) return;
    setTodos([...todos, { id: Date.now(), text: text.trim(), done: false }]);
    setText('');
  };

  const done = todos.filter(todo => todo.done).length;

  return (
    <div>
      <form onSubmit={add} style={{ display: 'flex', gap: 8 }}>
        <input className="input" value={text} onChange={event => setText(event.target.value)} placeholder="Something to do ..." aria-label="New todo" />
        <button type="submit">Add</button>
      </form>
      <div className="bar" style={{ margin: '14px 0 4px' }}><span style={{ width: todos.length ? `${done / todos.length * 100}%` : 0 }} /></div>
      <div className="muted small">{done} of {todos.length} done</div>
      <ul style={{ listStyle: 'none', padding: 0, margin: '8px 0 0' }}>
        {todos.map(todo => (
          <TodoItem key={todo.id} todo={todo}
                    onToggle={id => setTodos(todos.map(one => one.id === id ? { ...one, done: !one.done } : one))}
                    onDelete={id => setTodos(todos.filter(one => one.id !== id))} />
        ))}
      </ul>
      {todos.length === 0 && <div className="empty-state"><span className="big">🎉</span>All done.</div>}
    </div>
  );
}

function ComponentsDemo() {
  const [message, setMessage] = React.useState('No button pressed yet');

  return (
    <div className="cols-2">
      <div style={{ display: 'grid', gap: 16 }}>
        <Card title="A card">
          <p style={{ margin: 0 }}>Its title is a prop, this text is its <code>children</code>.</p>
        </Card>
        <Card title="Buttons, three variants" tone="pad">
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            <Button onClick={() => setMessage('The primary one')}>Primary</Button>
            <Button variant="btn-good" onClick={() => setMessage('The good one')}>Good</Button>
            <Button variant="btn-danger" onClick={() => setMessage('The dangerous one')}>Danger</Button>
          </div>
          <p className="muted small" style={{ margin: '12px 0 0' }}>Pressed: <strong>{message}</strong></p>
        </Card>
        <Card title="Cards in cards" tone="good">
          <Card title="Inner" tone="warn"><p style={{ margin: 0 }}>Composition all the way down.</p></Card>
        </Card>
      </div>
      <Card title="A todo list">
        <TodoApp />
      </Card>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('components-demo')).render(<ComponentsDemo />);