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<string>()</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);
}