rvx
Version:
A signal based rendering library
412 lines (329 loc) • 8.96 kB
Markdown
# Components
In rvx, components are functions that return any type of [supported content](elements.md#content).
=== "JSX"
```jsx
function Message() {
return <h1>Hello World!</h1>;
}
<Message />
```
=== "No Build"
```jsx
function Message() {
return e("h1").append("Hello World!");
}
Message()
```
## Properties
Properties are passed via the first argument as is:
=== "JSX"
```jsx
function Message(props: { text: string; }) {
return <h1>{props.text}</h1>;
}
<Message text="Hello World!" />
```
=== "No Build"
```jsx
/**
* @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.
=== "JSX"
```jsx
function Message(props: { children?: unknown; }) {
return <h1>{props.children}</h1>;
}
<Message>Hello World!</Message>
```
=== "No Build"
When not using JSX, the `children` property is still recommended for consistency, but not a requirement.
```jsx
/**
* @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`](signals.md#expressions) type.
=== "JSX"
```jsx
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} />
```
=== "No Build"
```jsx
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:
=== "JSX"
```jsx
import { Reactive } from "rvx";
function Counter(props: { value: Reactive<number>; }) {
return <>Current count: {props.value}</>;
}
```
=== "No Build"
```jsx
/**
* @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](signals.md) as properties.
=== "JSX"
```jsx
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} />
```
=== "No Build"
```jsx
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`](../convert.md#trim) function for trimming user input:
=== "JSX"
```jsx
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(...)} />
```
=== "No Build"
```jsx
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.
=== "JSX"
```jsx
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>;
}
```
=== "No Build"
```jsx
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:
=== "JSX"
```jsx
import { ClassValue, StyleValue } from "rvx";
function Button(props: {
class?: ClassValue;
style?: StyleValue;
...
}) {
return <button
class={[props.class, "example"]}
style={[props.style, { color: "red" }]}
...
>...</button>;
}
```
=== "No Build"
```jsx
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.
=== "JSX"
```jsx
import { Overwrite } from "rvx";
<Overwrite class="extra-class" on:keydown={...}>
<SomeComponent />
</Overwrite>
function SomeComponent() {
return <input ... />;
}
```
=== "No Build"
```jsx
import { e, overwrite } from "./rvx.js";
overwrite(
SomeComponent()
).class("extra-class").on("keydown", ...)
function SomeComponent() {
return e("input")...;
}
```
## Lifecycle Hooks
[Lifecycle hooks](./lifecycle.md) are supported in components:
=== "JSX"
```jsx
import { teardown } from "rvx";
function Timer() {
const elapsed = $(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}
```
=== "No Build"
```jsx
import { teardown } from "./rvx.js";
function Timer() {
const elapsed = $(0);
const timer = setInterval(() => { elapsed.value++ }, 1000);
teardown(() => clearInterval(timer));
return elapsed;
}
```