Forms & Data Binding

Because CellUI does not use a compiler (unlike Svelte's .svelte parser or Vue's v-model), we adhere to a strictly one-way data flow architecture.

You must explicitly wire input elements to their backing Signals.

One-Way Data Flow

To update a signal when a user types into an , you assign an oninput handler. To ensure the reflects the current value of the Signal, you bind the value attribute to it.

import { signal, view } from '@cmj/cellui';

function RegistrationForm() {
  const username = signal('');
  const password = signal('');

  const submit = (e: Event) => {
    e.preventDefault();
    console.log("Registering:", username.value, password.value);
  }

  return view`
    <form onsubmit="${submit}">
      <label>
        Username:
        <input 
          type="text" 
          value="${username}" 
          oninput="${(e: Event) => username.value = (e.target as HTMLInputElement).value}" 
        />
      </label>
      
      <label>
        Password:
        <input 
          type="password" 
          value="${password}" 
          oninput="${(e: Event) => password.value = (e.target as HTMLInputElement).value}" 
        />
      </label>

      <button type="submit">Register</button>
      
      <p>Live Preview: ${username}</p>
    </form>
  `;
}

Why not Two-Way Data Binding?

While rewriting (e) => signal.value = e.target.value repeatedly can cause "boilerplate fatigue", it maintains CellUI's commitment to explicitness.

There is no hidden synchronization magic happening behind the scenes. The oninput function executes strictly inside standard Javascript rules.

Checkboxes and Radios

For booleans and enumerations, bind the native event slightly differently:

function Preferences() {
  const receiveEmails = signal(false);
  const theme = signal('light'); // 'light' | 'dark'

  return view`
    <fieldset>
      <legend>Settings</legend>

      <label>
        <input 
          type="checkbox" 
          checked="${receiveEmails}" 
          onchange="${(e: Event) => receiveEmails.value = (e.target as HTMLInputElement).checked}" 
        />
        Subscribe to Newsletter
      </label>

      <div>
        <label>
          <input 
            type="radio" 
            name="theme" 
            value="light" 
            checked="${() => theme.value === 'light'}"
            onchange="${() => theme.value = 'light'}" 
          />
          Light Mode
        </label>
        <label>
          <input 
            type="radio" 
            name="theme" 
            value="dark" 
            checked="${() => theme.value === 'dark'}"
            onchange="${() => theme.value = 'dark'}" 
          />
          Dark Mode
        </label>
      </div>
    </fieldset>
  `;
}

Note: The snippet uses a function ${() => theme.value === 'dark'} for the checked boolean. Passing a function ensures CellUI re-evaluates the boolean whenever the dependent signal changes.