ref() Multi

ref() Multi demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import State, { Component, ref } from '@expressive/react';
import type { ChangeEvent } from 'react';

export default () => (
  <div className="container">
    <h1>Ref Proxy</h1>
    <p>
      <code>ref(this)</code> is the plural form: one call, one handle per field,
      keyed by name. Give it a factory and each key maps to whatever the call site
      wants instead - here the props for that field's input, so the state hands out
      its own wiring. Nothing below names a fader; the desk walks the state's keys.
    </p>
    <Desk />
    <small>
      Add <code>presence = 20</code> to Bands and a fifth fader appears, labelled
      and wired, because the list was never written down anywhere. The buttons use
      the plain proxy - <code>refs[band].current</code> writes a field the loop
      never had to name. Only <code>value</code> stays in render: that read is the
      subscription.
    </small>
  </div>
);

class Bands extends State {
  bass = 40;
  mids = 65;
  treble = 30;
  air = 55;

  fader = ref(this, (key, bands) => {
    const band = key as Band;

    return {
      type: 'range',
      min: 0,
      max: 100,
      onChange: (event: ChangeEvent<HTMLInputElement>) => {
        bands[band] = event.target.valueAsNumber;
      }
    };
  });

  refs = ref(this);
}

type Band = Exclude<State.Field<Bands>, 'fader' | 'refs'>;

class Desk extends Component {
  bands = new Bands();

  nudge(by: number) {
    const { refs } = this.bands;

    for (const band of faders(this.bands))
      refs[band].current = clamp(refs[band].current + by);
  }

  flatten() {
    const { refs } = this.bands;

    for (const band of faders(this.bands)) refs[band].current = 50;
  }

  render() {
    const { bands } = this;

    return (
      <section className="desk">
        {faders(bands).map((band) => (
          <label key={band}>
            <span>{band}</span>
            <input {...bands.fader[band]} value={bands[band]} />
            <output>{bands[band]}</output>
          </label>
        ))}

        <footer>
          <button onClick={() => this.nudge(-10)}>−10 all</button>
          <button onClick={() => this.flatten()}>Flatten</button>
          <button onClick={() => this.nudge(10)}>+10 all</button>
        </footer>
      </section>
    );
  }
}

// `refs` is non-enumerable, like every instruction, so a state's own keys are
// exactly its faders. Read them off `is`: the tracking proxy a render sees
// forwards property access, not enumeration.
const faders = (bands: Bands) => Object.keys(bands.is) as Band[];

const clamp = (value: number) => Math.max(0, Math.min(100, value));

App.css

.desk {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  width: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.desk label {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-size: var(--t-sm);
}

.desk label span {
  flex: 0 0 4rem;
  text-align: left;
  color: var(--muted);
  text-transform: capitalize;
}

.desk label output {
  flex: 0 0 2.5rem;
  font-family: var(--font-mono);
  text-align: right;
  color: var(--accent);
}

.desk footer {
  display: flex;
  gap: var(--s2);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}

.desk footer button {
  flex: 1;
  font-size: var(--t-sm);
}