UNPKG

rvx

Version:

A signal based rendering library

265 lines (202 loc) 8.51 kB
# 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](../elements.md#content) or can be returned from [component functions](../components.md). ## Creating Views Rvx provides the following views for common use cases: + [`render` & `mount`](./render.md) - Create a view from arbitrary content. + [`<Show> / when`](show.md) - Render content if a condition is met. + [`<Attach> / attachWhen`](attach.md) - Attach already rendered content if a condition is met. + [`<Nest> / nest`](nest.md) - Render a component returned from an expression. + [`<For> / forEach`](for-each.md) - Render content for each unique value in an iterable. + [`<Index> / indexEach`](index-each.md) - Render content for each index/value pair in an iterable. + [`movable`](movable.md) - Wrap content for safely moving it somewhere else. ## View API !!! danger As a direct consumer of the view API, you need to guarantee that: + The sequence of nodes inside the view is not modified from the outside. + If there are multiple nodes, all nodes must have a common parent node at all time. You can use `assertViewState` in development to validate the current view state: === "JSX" ```jsx import { assertViewState } from "rvx/test"; assertViewState(someView); ``` === "No Build" ```jsx import { assertViewState } from "./rvx.test.js"; assertViewState(someView); ``` The current boundary can be access via the `first` and `last` properties. ```jsx 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](../lifecycle.md) is disposed. Note, that there can be only one boundary owner at a time. ```jsx view.setBoundaryOwner((first, last) => { // "first" and "last" are the new current boundary. }); ``` To move or detach a view, use the `appendTo`, `insertBefore` and `detach` functions. They ensure, that a view doens't break when moving or detaching a view with multiple nodes. ```jsx // Append all nodes of the view to an element: view.appendTo(someElement); // Insert all nodes of the view before a reference node: view.insertBefore(parent, someChild); // Detach the view from it's current position: view.detach(); ``` ## Implementing Views !!! tip Before implementing your own view, consider using one of the [already existing](#creating-views) views. Custom views are usually only needed for very special (often performance critical) use cases involving a large number of elements to render. !!! danger When implementing your own view, you need to guarantee the following: + 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. + When the [lifecycle](../lifecycle.md) the view was created in is disposed, it's content is no longer updated in any way and no nodes are removed. You can use `assertViewState` in development to validate the current view state: === "JSX" ```jsx import { assertViewState } from "rvx/test"; assertViewState(someView); ``` === "No Build" ```jsx import { assertViewState } from "./rvx.test.js"; assertViewState(someView); ``` A view is created using the `View` constructor. The example below creates a view that consists of a single text node: === "JSX" ```jsx 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); }); ``` === "No Build" ```jsx 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); }); ``` !!! danger The `self` parameter is the view that is currently being created. Before the boundary is initialized, `first`, `last` and `parent` may return `undefined` and using anything else will result in undefined behavior. Most of the view implementations provided by rvx are returned from component functions like in the example below: === "JSX" ```jsx function ExampleView(props: { message: string }) { return new View((setBoundary, self) => { const node = document.createTextNode(props.message); setBoundary(node, node); }); } <ExampleView message="Hello World!" /> ``` === "No Build" ```jsx 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: === "JSX" ```jsx import { View, Emitter, Event } 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"); ``` === "No Build" ```jsx import { e, View, Emitter } from "./rvx.js"; /** * @param {object} props * @param {import("./rvx.js").Event<[string]>} props.messages */ 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. } }); }); } /** @type {Emitter<[string]>} */ 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 here: + [rvx core views](https://github.com/mxjp/rvx/blob/main/src/core/view.ts) + Examples + [Rotate](../../../examples/view-rotate.md) + [Push](../../../examples/view-push.md) ## Lifecycle Conventions When the [lifecycle](../lifecycle.md) in which a view was created is disposed, all of it's nodes should remain in place by convention. + This results in much better performance when disposing large amounts of nested views. + Users of that view have the ability to keep displaying remaining content for animation purposes.