Skip to content

Views

Views are an abstraction for sequences of DOM nodes that may change over time. To keep track of their position they contain at least one node which may be a comment node if there is nothing to render.

Views can be used as element content or can be returned from component functions.

Creating Views

Rvx provides the following views for common use cases:

  • render & mount - Wrap content in a view.
  • <Show> - Render content if a condition is met.
  • <Attach> - Attach already rendered content if a condition is met.
  • <Nest> - Render a component returned from an expression.
  • <For> - Render content for each unique value in an iterable.
  • <IndexFor> - Render content for each index in an iterable.
  • movable - Wrap content for safely moving it somewhere else.

View API

As a consumer of the view API, you need to guarantee that:

  • The sequence of nodes is not modified from the outside.
  • If there are multiple nodes, all nodes must have a common parent node at all time.

The current boundary can be access via the first and last properties.

console.log(view.first, view.last);

A callback that is called for any boundary updates (known as the boundary owner) can be set until the current lifecycle is disposed. Note, that there can be only one boundary owner at a time.

view.setBoundaryOwner((first, last) => {
    // "first" and "last" are the new current boundary.
});

To move or detach a view, use the take and detach functions. They ensure, that a view doens't break when moving or detaching a view with multiple nodes.

// Append all nodes of the view to an element:
someElement.append(view.take());

// Detach the view from it's current position:
view.detach();

Implementing Views

Before implementing your own view, consider using one of the already existing views. Custom views are usually only needed for very special (often performance critical) use cases involving a large number of elements to render.

When implementing your own view, you need to guarantee that:

  • The view doesn't break when the parent node is replaced or when a view consisting of only a single node is detached from it's parent.
  • The boundary is updated immediately after the first or last node has been updated.
  • There is at least one node at all time.
  • If there are multiple nodes, all nodes remain in the current parent.
  • If there are multiple nodes, the initial nodes must have a common parent.
  • When changing nodes, the view must remain in it's current position.

A view is created using the View constructor. The example below creates a view that consists of a single text node:

import { View } from "rvx";

const view = new View((setBoundary, self) => {
    // "self" is this view instance.

    const node = document.createTextNode("Hello World!");

    // Set the initial first and last node:
    // (This must be called at least once before this callback returns)
    setBoundary(node, node);
});
import { View } from "./rvx.js";

const view = new View((setBoundary, self) => {
    // "self" is this view instance.

    const node = document.createTextNode("Hello World!");

    // Set the initial first and last node:
    // (This must be called at least once before this callback returns)
    setBoundary(node, node);
});

Most of the view implementations provided by rvx are returned from component functions like in the example below:

function ExampleView(props: { message: string }) {
    return new View((setBoundary, self) => {
        const node = document.createTextNode(props.message);
        setBoundary(node, node);
    });
}

<ExampleView message="Hello World!" />
function ExampleView(props: { message: string }) {
    return new View((setBoundary, self) => {
        const node = document.createTextNode(props.message);
        setBoundary(node, node);
    });
}

ExampleView({ message: "Hello World!" })

The example below appends an element every time an event is fired:

import { View, Event, Emitter } from "rvx";

function LogEvents(props: { messages: Event<[string]> }) {
    return new View((setBoundary, self) => {
        // Create a placeholder node:
        // In this example, this will always be the last node of the view.
        const placeholder = document.createComment("");
        setBoundary(placeholder, placeholder);

        props.messages(message => {
            // Ensure, that there is a parent node to append to:
            let parent = self.parent;
            if (!parent) {
                parent = document.createDocumentFragment();
                parent.appendChild(placeholder);
            }

            // Create & insert the new node before the placeholder:
            const node = <li>{message}</li> as Node;
            parent.insertBefore(node, placeholder);

            // If this is the first message to append, update the boundary:
            if (placeholder === self.first) {
                setBoundary(node, undefined);
                // After this, the view boundary will always consist
                // of the first appended message and the placeholder.
            }
        });
    });
}

const messages = new Emitter<[string]>();

<ul>
    <LogEvents messages={messages.event} />
</ul>

messages.emit("Foo");
messages.emit("Bar");
import { View, Event, Emitter, e } from "./rvx.js";

function LogEvents(props) {
    return new View((setBoundary, self) => {
        // Create a placeholder node:
        // In this example, this will always be the last node of the view.
        const placeholder = document.createComment("");
        setBoundary(placeholder, placeholder);

        props.messages(message => {
            // Ensure, that there is a parent node to append to:
            let parent = self.parent;
            if (!parent) {
                parent = document.createDocumentFragment();
                parent.appendChild(placeholder);
            }

            // Create & insert the new node before the placeholder:
            const node = e("li").append(message).elem;
            parent.insertBefore(node, placeholder);

            // If this is the first message to append, update the boundary:
            if (placeholder === self.first) {
                setBoundary(node, undefined);
                // After this, the view boundary will always consist
                // of the first appended message and the placeholder.
            }
        });
    });
}

const messages = new Emitter<[string]>();

e("ul").append(
    LogEvents({ messages: messages.event })
)

messages.emit("Foo");
messages.emit("Bar");

You can find more complex view implementation examples in rvx's core view module and this example.