Private
Private 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, State, has } from '@expressive/mvc';
const server = createServer();
export default () => (
<div className="container">
<h1>Private fields</h1>
<p>
Sign in, then load your notes. <code>user</code> and <code>notes</code>{' '}
are state the UI renders. The session token is <code>#token</code> - a
JavaScript private field on the <code>Session</code> the view owns. Only the class's own methods reach it, and the
reactive system never sees it: it is not rendered, not exported by{' '}
<code>get()</code>, and cannot be set from outside.
</p>
<Account />
<small>
Use <code>#</code> fields from methods. Getters and{' '}
<code>render()</code> run against a tracking proxy, which cannot read
them.
</small>
</div>
);
class Session extends State {
user = '';
busy = false;
notes = has<string>();
#token = '';
async signIn(name: string) {
this.busy = true;
this.#token = await server.signIn(name);
this.user = name;
this.busy = false;
}
async load() {
this.busy = true;
this.notes.clear();
this.notes.push(...(await server.notes(this.#token)));
this.busy = false;
}
signOut() {
this.#token = '';
this.user = '';
this.notes.clear();
}
}
class Account extends Component {
name = 'Ada';
session = new Session();
render() {
const { name, session } = this;
const { user, busy, notes } = session;
if (user)
return (
<div className="session">
<p>Signed in as <b>{user}</b></p>
<div className="actions">
<Button primary onClick={session.load} disabled={busy}>Load notes</Button>
<Button onClick={session.signOut}>Sign out</Button>
</div>
<ul>
{notes.map((note, i) => <li key={i}>{note}</li>)}
</ul>
</div>
);
return (
<div className="session">
<label>
Name
<input value={name} onInput={(e) => (this.name = e.currentTarget.value)} />
</label>
<Button primary onClick={() => session.signIn(name)} disabled={busy || !name}>
Sign in
</Button>
</div>
);
}
}
function createServer() {
const sessions = new Map<string, string>();
const wait = () => new Promise((resolve) => setTimeout(resolve, 300));
return {
async signIn(name: string) {
await wait();
const token = Math.random().toString(36).slice(2);
sessions.set(token, name);
return token;
},
async notes(token: string) {
await wait();
const name = sessions.get(token);
if (!name) throw new Error('Not signed in.');
return [`Welcome back, ${name}.`, 'Your token never left the Session class.'];
}
};
}
App.css
.session {
display: flex;
flex-direction: column;
gap: var(--s3);
align-items: flex-start;
}
.session label {
display: flex;
flex-direction: column;
gap: var(--s1);
color: var(--fg);
}
.session input {
padding: var(--s2);
font: inherit;
}
.session .actions {
display: flex;
gap: var(--s2);
}
.session p {
margin: 0;
}