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 } from "rvx";
function Counter(props: { value: Expression<number>; }) {
return <>Current count: {props.value}</>;
}
const count = $(42);
// Static values:
<Counter value={count.value} />
// Reactive values:
<Counter value={count} />
<Counter value={() => count.value} />
import { $ } from "./rvx.js";
/**
* @param {object} props
* @param {import("./rvx.js").Expression<number>} props.value
*/
function Counter(props) {
return ["Current count: ", props.value];
}
const count = $(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 } from "rvx";
function Counter(props: { value: Signal<number>; }) {
return <button on:click={() => { props.value.value++ }}>
Count: {props.value}
</button>;
}
const count = $(0);
<Counter value={count} />
import { $, 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 = $(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, 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>) {
// The second parameter is metadata to let other APIs
// know that "input" has been derived from "source":
const input = $(source.value, source);
// 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 = $("");
// 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 derivations:
<TextInput value={text.pipe(trim).pipe(...)} />
import { $, 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) {
// The second parameter is metadata to let other APIs
// know that "input" has been derived from "source":
const input = $(source.value, source);
// 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 = $("");
// 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 derivations:
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 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(...);
}
Overwriting properties
For components that always return a top level element, you can overwrite properties that are not explicitly supported by that component.
Warning
Note, that styles, classes, attributes and properties that are already set by the component may cause conflicts.
import { Overwrite } from "rvx";
<Overwrite class="extra-class" on:keydown={...}>
<SomeComponent />
</Overwrite>
function SomeComponent() {
return <input ... />;
}
import { e, overwrite } from "./rvx.js";
overwrite(
SomeComponent()
).class("extra-class").on("keydown", ...)
function SomeComponent() {
return e("input")...;
}
Lifecycle Hooks
Lifecycle hooks are supported in components:
import { teardown } from "rvx";
function Timer() {
const elapsed = $(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}
import { teardown } from "./rvx.js";
function Timer() {
const elapsed = $(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}