Forms
Forms demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.
App.tsx
import './App.css';
// Form is kept separate as a reusable base class.
import { Form } from "./Form";
import { Preview } from "./Preview";
// Extending Form inherits the plumbing; can focus on values and layout.
class MyForm extends Form {
firstname = '';
lastname = '';
email = '';
// Methods and behavior are entirely up to you.
submit(){
if(!this.firstname || !this.lastname || !this.email){
alert('Please fill out all fields');
return;
}
alert(`Submitting ${this.firstname} ${this.lastname} with email ${this.email}`);
}
// Optional: render makes this self-contained.
// Without it, children pass through in context of the form.
render(){
const { to, submit } = this;
return (
<div className="form">
<h1>Example Form</h1>
<input ref={to.firstname} placeholder="Firstname" />
<input ref={to.lastname} placeholder="Lastname" />
<input ref={to.email} placeholder="Email Address" />
<button onClick={submit}>Submit</button>
<Preview />
</div>
);
}
}
// And just like that, we have a form component!
export default MyForm;
App.css
.form {
display: flex;
flex-direction: column;
gap: var(--s3);
width: 100%;
max-width: 30rem;
margin: 0 auto;
}
.form h1 {
margin: 0 0 var(--s2);
}
/* Submit hugs its content rather than stretching to the column width. */
.form button {
align-self: flex-start;
}
/* Live-preview panel sits a touch below the controls. */
.form pre {
margin: var(--s3) 0 0;
}
Form.tsx
import { Component, ref } from '@expressive/react';
// Base class: binds inputs and provides context to children.
export class Form extends Component {
// Use `ref` instruction to map over known keys of `this`,
// running a factory per key. Returns goto `this.to[key]`
// - for example, a react ref which binds each input.
to = ref(this, (key) => {
let reset: (() => void) | undefined;
return (el: HTMLInputElement | null) => {
if (reset) reset();
if (!el) return;
reset = this.bind(el, key);
}
})
// Bind input to its matching property; set name from key.
// Separate method so subclasses could override.
public bind(input: HTMLInputElement, key: keyof this) {
if (!(key in this) || typeof key !== "string")
throw new Error(`${this} has no property "${String(key)}"`);
if(!input.name) input.name = key;
const onInput = () => {
(this as any)[key] = input.value;
};
const unwatch = this.set(key, () => {
input.value = this[key] as string;
});
input.addEventListener('input', onInput);
return () => {
input.removeEventListener('input', onInput);
unwatch();
};
}
}Preview.tsx
import { Form } from "./Form";
// Simple component to show live values for any form.
export function Preview() {
// Access the nearest Form instance - MyForm included.
const form = Form.get();
const values = {} as Record<string, any>;
for (const key in form)
// Accessed properties subscribe to updates.
values[key] = (form as any)[key];
return (
<pre>{JSON.stringify(values, null, 2)}</pre>
);
}