rvx
Version:
A signal based rendering library
415 lines • 14.2 kB
JavaScript
import { NODE } from "./element-common.js";
import { ENV } from "./env.js";
import { createText } from "./internals/create-text.js";
import { NOOP } from "./internals/noop.js";
import { isolate } from "./isolate.js";
import { capture, teardown } from "./lifecycle.js";
import { $, get, memo, watch } from "./signals.js";
function createPlaceholder(env) {
return env.document.createComment("g");
}
function createParent(env) {
return env.document.createDocumentFragment();
}
function empty(setBoundary, env) {
const node = createPlaceholder(env);
setBoundary(node, node);
}
function use(setBoundary, node, env) {
if (node.firstChild === null) {
empty(setBoundary, env);
}
else {
setBoundary(node.firstChild, node.lastChild);
}
}
export function render(content) {
if (content instanceof View) {
return content;
}
return new View(setBoundary => {
const env = ENV.current;
if (Array.isArray(content)) {
const flat = content.flat(Infinity);
if (flat.length > 1) {
const parent = createParent(env);
for (let i = 0; i < flat.length; i++) {
let part = flat[i];
if (part === null || part === undefined) {
parent.appendChild(createPlaceholder(env));
}
else if (typeof part === "object") {
if (NODE in part) {
part = part[NODE];
}
if (part instanceof env.Node) {
if (part.nodeType === 11 && part.childNodes.length === 0) {
parent.appendChild(createPlaceholder(env));
}
else {
parent.appendChild(part);
}
}
else if (part instanceof View) {
part.appendTo(parent);
if (i === 0) {
part.setBoundaryOwner((first, _last) => setBoundary(first, undefined));
}
else if (i === flat.length - 1) {
part.setBoundaryOwner((_first, last) => setBoundary(undefined, last));
}
}
else {
parent.appendChild(createText(part, env));
}
}
else {
parent.appendChild(createText(part, env));
}
}
use(setBoundary, parent, env);
return;
}
content = flat[0];
}
if (content === null || content === undefined) {
empty(setBoundary, env);
}
else if (typeof content === "object") {
if (NODE in content) {
content = content[NODE];
}
if (content instanceof env.Node) {
if (content.nodeType === 11) {
use(setBoundary, content, env);
}
else {
setBoundary(content, content);
}
}
else if (content instanceof View) {
setBoundary(content.first, content.last);
content.setBoundaryOwner(setBoundary);
}
else {
const text = createText(content, env);
setBoundary(text, text);
}
}
else {
const text = createText(content, env);
setBoundary(text, text);
}
});
}
export function mount(parent, content) {
const view = render(content);
view.appendTo(parent);
teardown(() => view.detach());
return view;
}
export class View {
#first;
#last;
#owner;
constructor(init) {
init((first, last) => {
if (first) {
this.#first = first;
}
if (last) {
this.#last = last;
}
this.#owner?.(this.#first, this.#last);
}, this);
if (!this.#first || !this.#last) {
throw new Error("G1");
}
}
get first() {
return this.#first;
}
get last() {
return this.#last;
}
get parent() {
return this.#first?.parentNode ?? undefined;
}
setBoundaryOwner(owner) {
if (this.#owner !== undefined) {
throw new Error("G2");
}
this.#owner = owner;
teardown(() => this.#owner = undefined);
}
appendTo(parent) {
let node = this.#first;
const last = this.#last;
for (;;) {
const next = node.nextSibling;
parent.appendChild(node);
if (node === last) {
break;
}
node = next;
}
}
insertBefore(parent, ref) {
if (ref === null) {
return this.appendTo(parent);
}
let node = this.#first;
const last = this.#last;
for (;;) {
const next = node.nextSibling;
parent.insertBefore(node, ref);
if (node === last) {
break;
}
node = next;
}
}
detach() {
const first = this.#first;
const last = this.#last;
if (first === last) {
first.parentNode?.removeChild(first);
return first;
}
else {
const fragment = ENV.current.document.createDocumentFragment();
this.appendTo(fragment);
return fragment;
}
}
}
export function* viewNodes(view) {
let node = view.first;
for (;;) {
const next = node.nextSibling;
yield node;
if (node === view.last) {
break;
}
node = next;
}
}
const _nestDefault = ((component) => component?.());
export function nest(expr, component = _nestDefault) {
return new View((setBoundary, self) => {
watch(expr, value => {
const last = self.last;
const parent = last?.parentNode;
let view;
if (parent) {
const anchor = last.nextSibling;
self.detach();
view = render(component(value));
view.insertBefore(parent, anchor);
}
else {
view = render(component(value));
}
setBoundary(view.first, view.last);
view.setBoundaryOwner(setBoundary);
});
});
}
export function Nest(props) {
return nest(props.watch, props.children);
}
export function when(condition, truthy, falsy) {
return nest(memo(condition), value => value ? truthy(value) : falsy?.());
}
export function Show(props) {
return when(props.when, props.children, props.else);
}
export function forEach(each, component) {
return new View((setBoundary, self) => {
function detach(instances) {
for (let i = 0; i < instances.length; i++) {
instances[i].v.detach();
}
}
const env = ENV.current;
let cycle = 0;
const instances = [];
const instanceMap = new Map();
const first = createPlaceholder(env);
setBoundary(first, first);
teardown(() => {
for (let i = 0; i < instances.length; i++) {
instances[i].d();
}
});
watch(() => {
let parent = self.parent;
if (!parent) {
parent = createParent(env);
parent.appendChild(first);
}
let index = 0;
let last = first;
try {
for (const value of get(each)) {
let instance = instances[index];
if (instance && Object.is(instance.u, value)) {
instance.c = cycle;
instance.i.value = index;
last = instance.v.last;
index++;
}
else {
instance = instanceMap.get(value);
if (instance === undefined) {
const instance = {
u: value,
c: cycle,
i: $(index),
d: undefined,
v: undefined,
};
instance.d = isolate(capture, () => {
instance.v = render(component(value, () => instance.i.value));
instance.v.setBoundaryOwner((_, last) => {
if (instances[instances.length - 1] === instance && instance.c === cycle) {
setBoundary(undefined, last);
}
});
});
instance.v.insertBefore(parent, last.nextSibling);
instances.splice(index, 0, instance);
instanceMap.set(value, instance);
last = instance.v.last;
index++;
}
else if (instance.c !== cycle) {
instance.i.value = index;
instance.c = cycle;
const currentIndex = instances.indexOf(instance, index);
if (currentIndex < 0) {
detach(instances.splice(index, instances.length - index, instance));
instance.v.insertBefore(parent, last.nextSibling);
}
else {
detach(instances.splice(index, currentIndex - index));
}
last = instance.v.last;
index++;
}
}
}
}
finally {
if (instances.length > index) {
detach(instances.splice(index));
}
for (const [value, instance] of instanceMap) {
if (instance.c !== cycle) {
instanceMap.delete(value);
instance.v.detach();
instance.d();
}
}
cycle = (cycle + 1) | 0;
setBoundary(undefined, last);
}
});
});
}
export function For(props) {
return forEach(props.each, props.children);
}
export function indexEach(each, component) {
return new View((setBoundary, self) => {
const env = ENV.current;
const first = createPlaceholder(env);
setBoundary(first, first);
const instances = [];
teardown(() => {
for (let i = 0; i < instances.length; i++) {
instances[i].d();
}
});
watch(() => {
let parent = self.parent;
if (!parent) {
parent = createParent(env);
parent.appendChild(first);
}
let index = 0;
let last = first;
try {
for (const value of get(each)) {
if (index < instances.length) {
const current = instances[index];
if (Object.is(current.u, value)) {
last = current.v.last;
index++;
continue;
}
current.v.detach();
current.d();
current.d = NOOP;
}
const instance = {
u: value,
d: undefined,
v: undefined,
};
instance.d = isolate(capture, () => {
instance.v = render(component(value, index));
instance.v.setBoundaryOwner((_, last) => {
if (instances[instances.length - 1] === instance) {
setBoundary(undefined, last);
}
});
});
instance.v.insertBefore(parent, last.nextSibling);
instances[index] = instance;
last = instance.v.last;
index++;
}
}
finally {
if (instances.length > index) {
for (let i = index; i < instances.length; i++) {
const instance = instances[i];
instance.v.detach();
instance.d();
}
instances.length = index;
}
setBoundary(undefined, last);
}
});
});
}
export function Index(props) {
return indexEach(props.each, props.children);
}
export class MovableView {
#view;
#target = $();
constructor(view) {
this.#view = view;
}
move = () => {
this.#target.value = undefined;
const target = this.#target = $(this.#view);
return nest(target, v => v);
};
detach() {
this.#target.value = undefined;
}
}
export function movable(content) {
return new MovableView(render(content));
}
export function attachWhen(condition, content) {
return nest(condition, value => value ? content : undefined);
}
export function Attach(props) {
return attachWhen(props.when, props.children);
}
//# sourceMappingURL=view.js.map