Skip to content

Custom View

This example shows a custom view that rotates the order of it's logical children when a signal is updated.

You can toggle the visibility of the entire view or one of it's children to demonstrate boundary tracking.

import { Attach, View, render, sig } from "rvx";
import { Emitter, Event } from "rvx/event";

export function Example() {
    const rotate = new Emitter<[]>();
    const allVisible = sig(true);
    const innerVisible = sig(true);

    return <div class="column">
        <div class="row">
            <button on:click={() => { rotate.emit() }}>Rotate Order</button>
            <button on:click={() => allVisible.value = !allVisible.value}>Toggle Outer Visibility</button>
            <button on:click={() => innerVisible.value = !innerVisible.value}>Toggle Inner Visibility</button>
        </div>
        <Attach when={allVisible}>
            <RotateOrder on={rotate.event}>
                <input type="text" value="A" />
                <input type="text" value="BB" />

                {/* This "Attach" view is treated as a single child: */}
                <Attach when={innerVisible}>
                    <input type="text" value="CC1" />
                    <input type="text" value="CC2" />
                </Attach>
            </RotateOrder>
        </Attach>
    </div>;
}

function RotateOrder(props: {
    children?: unknown;
    on: Event<[]>;
}) {
    // Get a flat array of all children of this component:
    const children = [props.children].flat(Infinity);

    // Skip creating an actual view if there is only zero or one child:
    if (children.length < 2) {
        return children[0];
    }

    // Create a new view instance that can
    // be directly used as content in rvx:
    // (See the "View" constructor docs for the exact
    //  requirements of a custom view implementation.)
    return new View((setBoundary, self) => {
        // Create a child view for each child using the "render" function:
        const views = children.map(render);
        // Create an initial parent node:
        const parent = document.createDocumentFragment();

        // Views need to keep track of their first and last nodes.
        for (const view of views) {
            parent.appendChild(view.take());

            // Keep track of child view boundary updates to
            // update this view's boundary:
            view.setBoundaryOwner((first, last) => {
                // Update the first node of the current view if
                // this child view is currently the first one:
                if (view === views[0]) {
                    setBoundary(first, undefined);
                }
                // Update the last node of the current view if
                // this child view is currently the last one:
                if (view === views[views.length - 1]) {
                    setBoundary(undefined, last);
                }
            });
        }

        // Set the initial boundary:
        setBoundary(parent.firstChild!, parent.lastChild!);

        // Handle rotation events:
        props.on(() => {
            const parent = self.parent;
            if (parent) {
                // Rotate the order in the view array and DOM nodes:
                const view = views.pop()!;
                parent.insertBefore(view.take(), views[0].first);
                views.unshift(view);

                // Notify the custom view that our boundary has changed:
                setBoundary(view.first, views[views.length - 1].last);
            }

            // If the parent doesn't exist, we can assume that there
            // is at most one node. Rotating the child nodes can be
            // skipped in this case.
        });
    });
}