map() Create
map() Create 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, get, map } from '@expressive/react';
const PEOPLE = ['alice', 'bob', 'carol', 'dave'];
const STATUS = ['online', 'away', 'busy'];
// The room owns the people, keyed by id, and spawns them with a second
// argument: set(id, name) forwards to the factory. The detail reads
// people.get(selected), which subscribes to that one key - so it
// re-renders when the selected person changes, not when the others do.
export default class Room extends Component {
people = map((id: string) => new Person({ id, name: capitalize(id) }));
selected = '';
protected new() {
for (const name of PEOPLE) this.people.set(name);
[this.selected] = PEOPLE;
}
render() {
const { people, selected } = this;
const active = people.get(selected);
return (
<div className="container room">
<h1>Presence</h1>
<p>
Click a name to select; click a status dot to cycle it. The detail
reads <code>people.get(selected)</code>, so it tracks only that key.
</p>
<div className="layout">
<ul className="people">{people}</ul>
<aside className="detail">
{active ? (
<>
<span className={`dot lg ${active.status}`} />
<h2>{active.name}</h2>
<p className="status">{active.status}</p>
<button onClick={() => active.cycle()}>Cycle status</button>
</>
) : (
<p className="empty">Select someone</p>
)}
</aside>
</div>
</div>
);
}
}
// A person is a spawned, owned member keyed by id. It reads get(Room) to
// reflect and change the selection.
class Person extends Component {
room = get(Room);
id = '';
name = '';
status = 'online';
cycle() {
this.status = STATUS[(STATUS.indexOf(this.status) + 1) % STATUS.length];
}
render() {
const { id, name, status, room } = this;
return (
<li
className={room.selected === id ? 'person selected' : 'person'}
onClick={() => (room.selected = id)}>
<span className={`dot ${status}`} onClick={(e) => { e.stopPropagation(); this.cycle(); }} />
<span className="name">{name}</span>
</li>
);
}
}
function capitalize(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
App.css
.room {
max-width: 32rem;
}
.room .layout {
display: grid;
grid-template-columns: 1fr 12rem;
gap: var(--s4);
width: 100%;
align-items: start;
}
ul.people {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--s1);
}
.person {
display: flex;
align-items: center;
gap: var(--s3);
padding: var(--s2) var(--s3);
border: 1px solid transparent;
border-radius: var(--r);
cursor: pointer;
text-align: left;
}
.person:hover {
background: var(--surface);
}
.person.selected {
background: var(--accent-soft);
border-color: var(--accent);
}
.person .name {
font-size: var(--t-base);
color: var(--fg);
}
.dot {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
flex-shrink: 0;
cursor: pointer;
background: var(--muted);
}
.dot.lg {
width: 1rem;
height: 1rem;
}
.dot.online {
background: #10b981;
}
.dot.away {
background: #f59e0b;
}
.dot.busy {
background: #ef4444;
}
.detail {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--s2);
padding: var(--s4);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
text-align: center;
}
.detail h2 {
margin: 0;
font-size: var(--t-lg);
}
.detail .status {
margin: 0;
font-size: var(--t-sm);
color: var(--muted);
text-transform: capitalize;
}
.detail button {
margin-top: var(--s2);
padding: var(--s2) var(--s3);
font: inherit;
font-size: var(--t-sm);
color: var(--fg);
background: var(--bg);
border: 1px solid var(--border-2);
border-radius: var(--r-sm);
cursor: pointer;
}
.detail button:hover {
border-color: var(--accent);
}
.detail .empty {
color: var(--muted);
font-size: var(--t-sm);
}