Components
In rvx, components are functions that return any type of supported content.
function Message() {
return <h1>Hello World!</h1>;
}
<Message />
function Message() {
return e("h1").append("Hello World!");
}
Message()
Properties
Properties are passed via the first argument as is:
function Message(props: { text: string; }) {
return <h1>{props.text}</h1>;
}
<Message text="Hello World!" />
/**
* @param {object} props
* @param {string} props.text
*/
function Message(props) {
return e("h1").append(props.text);
}
Message({ text: "Hello World!" })
In JSX, children are passed as the children
property.
function Message(props: { children?: unknown; }) {
return <h1>{props.children}</h1>;
}
<Message>Hello World!</Message>
When not using JSX, the children
property is still recommended for consistency, but not a requirement.
/**
* @param {object} props
* @param {unknown} props.children
*/
function Message(props) {
return e("h1").append(props.children);
}
Message({ children: "Hello World!" })
Expressions
By default, all component properties are static. To accept reactive inputs, use the Expression
type.
import { Expression, sig } from "rvx";
function Counter(props: { value: Expression<number>; }) {
return <>Current count: {props.value}</>;
}
const count = sig(42);
// Static values:
<Counter value={count.value} />
// Reactive values:
<Counter value={count} />
<Counter value={() => count.value} />
import { sig } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").Expression<number>} props.value
*/
function Counter(props) {
return ["Current count: ", props.value];
}
const count = sig(42);
// Static values:
Counter({ count: 42 })
Counter({ count: count.value })
// Reactive values:
Counter({ count: count })
Counter({ count: () => count.value })
In cases where static values never make sense, you can use Reactive
instead of the Expression
type to disallow static values:
import { Reactive } from "rvx";
function Counter(props: { value: Reactive<number>; }) {
return <>Current count: {props.value}</>;
}
/**
* @param {object} props
* @param {import("./rvx.js").Reactive<number>} props.value
*/
function Counter(props) {
return ["Current count: ", props.value];
}
Signals
To support data flow in both directions, you can use signals as properties.
import { Signal, sig } from "rvx";
function Counter(props: { value: Signal<number>; }) {
return <button on:click={() => { props.value.value++ }}>
Count: {props.value}
</button>;
}
const count = sig(0);
<Counter value={count} />
import { sig, e } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").Signal<number>} props.value
*/
function Counter(props) {
return e("button").on("click", () => { props.value.value++ }).append(
"Count: ", props.value,
);
}
const count = sig(0);
Counter({ value: count })
Using signals for two way data flow also allows converting values in both directions in a nicely composable way.
The example below shows a basic text input and a trim
function for trimming user input:
import { Signal, sig, watchUpdates } from "rvx";
function TextInput(props: { value: Signal<string>; }) {
return <input
type="text"
prop:value={props.value}
on:input={event => {
props.value.value = (event.target as HTMLInputElement).value;
}}
/>;
}
function trim(source: Signal<string>) {
const input = sig(source.value);
// Update the source signal if the input changes:
watchUpdates(input, value => {
source.value = value.trim();
});
// Update the input signal if the source changes:
watchUpdates(source, value => {
if (value !== input.value.trim()) {
input.value = value;
}
});
return input;
}
const text = sig("");
// This input uses the "text" signal as is:
<TextInput value={text} />
// This input uses the "trim" function to store the
// trimmed version of the input in the "text" signal:
<TextInput value={trim(text)} />
// The signal's pipe function does the same but is more
// readable when using multiple conversions:
<TextInput value={text.pipe(trim).pipe(...)} />
import { sig, watchUpdates, e } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").Signal<string>} props.value
*/
function TextInput(props) {
return e("input")
.set("type", "text")
.prop("value", props.value)
.on("input", event => props.value.value = event.target.value);
}
/**
* @param {import("./rvx.js").Signal<string>} source
*/
function trim(source) {
const input = sig(source.value);
// Update the source signal if the input changes:
watchUpdates(input, value => {
source.value = value.trim();
});
// Update the input signal if the source changes:
watchUpdates(source, value => {
if (value !== input.value.trim()) {
input.value = value;
}
});
return input;
}
const text = sig("");
// This input uses the "text" signal as is:
TextInput({ value: text })
// This input uses the "trim" function to store the
// trimmed version of the input in the "text" signal:
TextInput({ value: trim(text) })
// The signal's pipe function does the same but is more
// readable when using multiple conversions:
TextInput({ value: text.pipe(trim).pipe(...) })
Forwarding special attributes
Sometimes it can be useful to forward properties to the root element of a component for allowing the user of the component to set the class
, style
or any other attributes.
import { ClassValue, StyleValue } from "rvx";
function Button(props: {
class?: ClassValue;
style?: StyleValue;
id?: Expression<string | undefined>;
...
}) {
return <button
class={props.class}
style={props.style}
id={props.id}
>...</button>;
}
import { e } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").ClassValue} props.class
* @param {import("./rvx.js").StyleValue} props.style
* @param {import("./rvx.js").Expression<string | undefined>} props.id
*/
function Button(props) {
return e("button")
.class(props.class)
.style(props.style)
.set("id", props.id)
.append(...);
}
In case of the class
and style
attributes, you can use an array as value to mix properties with values from within your component:
import { ClassValue, StyleValue } from "rvx";
function Button(props: {
class?: ClassValue;
style?: StyleValue;
...
}) {
return <button
class={[props.class, "example"]}
style={[props.style, { color: "red" }]}
...
>...</button>;
}
import { e } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").ClassValue} props.class
* @param {import("./rvx.js").StyleValue} props.style
*/
function Button(props) {
return e("button")
.class([props.class, "example"])
.style([props.style, { color: "red" }])
.append(...);
}
Lifecycle Hooks
Lifecycle hooks are supported in components:
import { teardown } from "rvx";
function Timer() {
const elapsed = sig(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}
import { teardown } from "./rvx.js";
function Timer() {
const elapsed = sig(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}