Unmanaged
Unmanaged demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.
App.tsx
import './App.css';
import Button from './common/Button';
import { Component } from '@expressive/mvc';
export default () => (
<div className="container">
<h1>Unmanaged fields</h1>
<p>
Type, then pause - the draft saves itself. A <code>_</code> prefix opts a
field out of state: <code>_timer</code> holds the pending save, so the
effect and <code>save()</code> can replace or clear it without a render.
<code>delay</code> stays state - the label tracks it - but the effect
reads it through <code>is</code>, a silent read. A <code>_</code> field
opts out everywhere; <code>is</code> opts out one read. Moving the slider
sets the next save's delay without restarting a pending one.
</p>
<Draft delay={600} />
</div>
);
class Draft extends Component {
text = '';
saved = '';
delay = 1000;
_timer?: ReturnType<typeof setTimeout>;
get status() {
const { text, saved } = this;
if (text !== saved) return 'Unsaved changes';
return saved ? 'Saved' : 'Nothing to save';
}
mount() {
this.get((current) => {
const { text, saved, is } = current;
clearTimeout(current._timer);
if (text !== saved)
current._timer = setTimeout(this.save, is.delay);
});
return () => clearTimeout(this._timer);
}
save() {
clearTimeout(this._timer);
this.saved = this.text;
}
render() {
const { text, saved, delay, status } = this;
return (
<div className="draft">
<textarea
aria-label="Draft"
value={text}
onInput={(e) => (this.text = e.currentTarget.value)}
/>
<label>
Delay: {delay}ms
<input
type="range"
min={200}
max={2000}
step={100}
value={delay}
onInput={(e) => (this.delay = +e.currentTarget.value)}
/>
</label>
<p className="status">{status}</p>
<Button onClick={this.save} disabled={text === saved}>
Save now
</Button>
</div>
);
}
}
App.css
.draft {
display: flex;
flex-direction: column;
gap: var(--s3);
align-items: flex-start;
}
.draft textarea {
width: 100%;
min-height: 6rem;
padding: var(--s2);
font: inherit;
color: var(--fg);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
resize: vertical;
}
.status {
margin: 0;
color: var(--fg);
}
.draft label {
display: flex;
flex-direction: column;
gap: var(--s1);
color: var(--fg);
}