has() List

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

App.tsx

import './App.css';

import { Component, has } from '@expressive/react';

// `has<string>()` is the list mode: an ordered collection of plain values you
// push, addressed by index. No spawned members, no keys - just a log. Reads
// track precisely, so `get(-1)` re-renders on a new tail, `size` on length.
export default class Editor extends Component {
  history = has<string>();
  draft = '';

  protected new() {
    this.history.push('open document');
  }

  commit(text = this.draft) {
    text = text.trim();
    if (!text) return;
    this.history.push(text);
    this.draft = '';
  }

  undo() {
    this.history.pop();
  }

  render() {
    const { history, draft } = this;

    return (
      <div className="container log">
        <h1>Owned List</h1>
        <p>
          <code>has&lt;string&gt;()</code> stores values by position. Push to
          append, pop to undo; <code>get(-1)</code> reads the latest entry.
        </p>

        <ol className="entries">
          {[...history].map((entry, i) => (
            <li key={i}>
              <span className="idx">{i}</span>
              <span className="entry">{entry}</span>
            </li>
          ))}
        </ol>

        <form
          onSubmit={(e) => {
            e.preventDefault();
            this.commit();
          }}>
          <input
            value={draft}
            placeholder="Record an action…"
            onChange={(e) => (this.draft = e.target.value)}
          />
          <button type="submit">Push</button>
        </form>

        <footer>
          <small>{history.size} entries · latest: {history.get(-1) ?? '—'}</small>
          <button className="ghost" onClick={() => this.undo()} disabled={!history.size}>
            Undo
          </button>
        </footer>
      </div>
    );
  }
}

App.css

.log {
  max-width: 28rem;
}

.entries {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.entries li {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  cursor: default;
}

.entries .idx {
  flex: 0 0 auto;
  min-width: 1.5rem;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--muted);
  text-align: right;
}

.entries .entry {
  flex: 1;
}

.log form {
  display: flex;
  gap: var(--s2);
  width: 100%;
}

.log form button {
  flex: 0 0 auto;
}

.log footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  width: 100%;
}

.log footer .ghost {
  padding: var(--s1) var(--s3);
  font-size: var(--t-sm);
}

.log footer .ghost:disabled {
  opacity: 0.5;
  cursor: default;
  border-color: var(--border);
}