Props
Props 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/react';
export default () => (
<div className="container">
<h1>Props</h1>
<p>
Every state field is an optional JSX prop, reapplied on each render - so
whoever passes one owns it. <code>unit</code> is declared on{' '}
<code>render</code> instead, which keeps it out of state and, being
non-optional there, makes it a required attribute.
</p>
<Dashboard />
<small>
CPU takes its value from above, so its own buttons only hold until the next
render up there reapplies the preset. Disk keeps its own, seeded once by{' '}
<code>is</code> at construction.
</small>
</div>
);
class Dashboard extends Component {
preset = 'idle';
presets: Record<string, number> = { idle: 8, busy: 62, peak: 97 };
render() {
const { preset, presets } = this;
return (
<>
<div className="row">
{Object.keys(presets).map((name) => (
<Button key={name} primary={name === preset} onClick={() => (this.preset = name)}>
{name}
</Button>
))}
</div>
<Gauge label="CPU" value={presets[preset]} unit="%" />
<Gauge label="Disk" max={512} step={32} unit=" GB" is={(disk) => (disk.value = 128)} />
</>
);
}
}
class Gauge extends Component {
label = 'Metric';
value = 0;
max = 100;
step = 10;
get percent() {
return Math.round((this.value / this.max) * 100);
}
bump(by: number) {
this.value = Math.min(this.max, Math.max(0, this.value + by));
}
render(props = {} as { unit: string }) {
const { label, value, percent, step } = this;
return (
<div className="gauge">
<header>
<span>{label}</span>
<output>
{value}
{props.unit}
</output>
</header>
<div className="track">
<div className="fill" style={{ width: `${percent}%` }} />
</div>
<footer>
<button onClick={() => this.bump(-step)}>−</button>
<button onClick={() => this.bump(step)}>+</button>
</footer>
</div>
);
}
}
App.css
.row {
display: flex;
gap: var(--s2);
}
.gauge {
width: 100%;
padding: var(--s4);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
text-align: left;
}
.gauge header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: var(--s2);
font-weight: 600;
}
.gauge output {
font-family: var(--font-mono);
font-size: var(--t-sm);
color: var(--accent);
}
.track {
height: 8px;
background: var(--surface-2);
border-radius: var(--r-sm);
overflow: hidden;
}
.fill {
height: 100%;
background: var(--accent);
transition: width 0.2s;
}
.gauge footer {
display: flex;
justify-content: flex-end;
gap: var(--s2);
margin-top: var(--s3);
}
.gauge footer button {
padding: 0 var(--s3);
line-height: 1.8;
}