Components are self-contained pieces of HTML and JS that you can use elsewhere in your app.
The this
value inside a component is automatically a Stateful<T>
, so you can put state on this
and have it automatically update.
Components return HTML elements when constructed, unless you manually return a Fragment or other data.
The Component
type has 3 separate objects: Props
which is all the JSX props passed to the componeont, Private
which is private state, and Public
which is public properties that aren't necessarily props. The Stateful<T>
type for this
in a component merges all 3 of these objects.
The first argument to a component is its ComponentContext
, which is where you can access children passed to the component, add callbacks, access the root element once constructed, or get the raw Stateful object assigned to this
.
let Button: Component<{ title: string; children?: ComponentChild }> = function (
cx
) {
return (
<div>
{use(this.title)}
<button>{cx.children}</button>
</div>
);
};
ComponentContext
has two main callbacks for initialization.
The most common one used is mount
, which is run only on the client side once the component's HTML has been constructed (but not necessarily after it's been added to the DOM).
The other one is init
, which runs in the same place as mount
but is run on both the client and server side. cx.root
, the component's HTML root, is accessible in both init
and mount
. You realistically can't touch the DOM in init
however, even though it's possible to do so, as the default server-side DOM is very minimal.
let ChartView: Component = function (cx) {
cx.mount = () => {
// mount the chart on the client once the canvas is constructed
let chart = new Chart(cx.root as HTMLCanvasElement, {
/* ... */
});
// update the chart on data changes ...
};
return <canvas />;
};
If you need to easily access DOM elements in the component that aren't the root, you can pass a pointer to the special prop this
:
let ChartCard: Component<{}, { chart: HTMLCanvasElement }> = function (cx) {
cx.mount = () => {
// mount the chart on the client once the canvas is constructed
let chart = new Chart(this.chart as HTMLCanvasElement, {
/* ... */
});
// update the chart on data changes ...
};
return (
<div>
<h1>Chart</h1>
<canvas this={use(this.chart)} />
</div>
);
};
Pointer<T>
s passed as props are automatically "proxied" to the component's this
value.
Effectively, any change to the pointer's value will change the property on the component's this
, and any change to the property on the component's this
will change the pointer's value.
This behavior is when the true power of the state system starts to show.
let Input: Component<{ title: string; text: string }, { hint: string }> =
function () {
this.hint = "";
use(this.text).listen((text) => {
if (text.length < 3) this.hint = "too short!";
if (text.length > 12) this.hint = "too long!";
});
return (
<div>
{this.title /* title won't change */}
<input value={use(this.text)} />
{use(this.hint)}
</div>
);
};
let SignUp: Component<{}, { user: string; password: string }> = function () {
let submit = () => {
// clear the inputs
this.user = "";
this.password = "";
};
return (
<div>
<h1>Sign Up</h1>
<Input title="Username: " text={use(this.user)} />
<Input title="Password: " text={use(this.password)} />
<button on:click={submit}>Submit</button>
</div>
);
};
When the user types in the inputs, the changes ripple outwards from the Input
's this.text
, then to the SignUp
's this.user
or this.passsword
.
Similarly, when the inputs are cleared in SignUp
, the Input
's this.text
will automatically get cleared and then clear the text in the input.