Components
Components are the foundational feature of the kivi library. They are tightly integrated with kivi Scheduler, and should be used as a basic building block for creating user interfaces. Component can be a simple HTML element, SVG element, or a canvas object. To update its representation it is possible to use direct DOM manipulations, Virtual DOM API, or draw on a canvas.
To create components, we need to declare its properties and behavior in ComponentDescriptor
instance, it acts as a
virtual table for component instances. Each component created from component descriptor will be automatically linked
to its descriptor.
ComponentDescriptor
TypeScript developers can provide types for props and state, ComponentDescriptor has two parameteric types: P
for
props type and S
for state type.
To create component instances, we can use one of the two methods: createComponent
, or createRootComponent
. The
difference between them is that root component doesn't have a parent component. For example:
class Props {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class State {
xy: number;
constructor(props: Props) {
this.xy = props.x * props.y;
}
}
const MyComponent = new ComponentDescriptor<Props, State>()
.canvas()
.init((c, props) => {
c.state = new State(props);
})
.update((c, props, state) => {
const ctx = c.get2DContext();
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
ctx.fillRect(props.x, props.y, state.xy, state.xy);
});
const componentInstance = MyComponent.createRootComponent(new Props(10, 20));
Setting component properties
Component descriptor instance has many different methods to set properties, all property methods support method chaining. For example:
const MyComponent = new ComponentDescriptor<number, void>()
.svg()
.tagName("a")
.update((c) => { c.element.width = this.props; });
List of basic properties:
ComponentDescriptor<P, S>.tagName(tagName: string): ComponentDescriptor<P, S>;
ComponentDescriptor<P, S>.svg(): ComponentDescriptor<P, S>;
Using Virtual DOM
To use virtual dom in components, we need to call a sync
method that will sync component's representation with a
virtual dom. For example:
const MyComponent = new ComponentDescriptor<{title: string, content: string}, void>()
.update((c, props) => {
c.sync(c.createVRoot()
.children([
createVElement("h1").child(props.title),
createVElement("p").child(props.content),
]));
});
To create virtual nodes that represent components, use component descriptor method createVNode(data?: D)
. For example:
const vnode = MyComponent.createVNode({
title: "Component Example",
content: "content",
});
Drawing on a canvas
To use canvas as a surface for component representation, enable canvas mode with component descriptor method
descriptor.canvas()
. For example:
const MyCanvasComponent = new ComponentDescriptor<void, void>()
.canvas()
.update((c) => {
const ctx = c.get2DContext();
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 10, 10);
});
Lifecycle methods
init
Init handler will be invoked after component state is created, element
and props
properties will be
initialized before init handler is invoked.
const MyComponent = new ComponentDescriptor<void, void>()
.tagName("button")
.init((c, props, state) => {
c.element.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
console.log("clicked");
});
})
.update((c) => {
c.sync(c.createVRoot().child("click me"));
});
update
Update handler will be invoked when component gets updated either by binding new props, or when it is registered in one of the scheduler component queues.
Update handler completely overrides default update behavior, so to continue using virtual dom for updates, we can
use sync
method.
const MyComponent = new ComponentDescriptor<{a: number}, void>()
.update((c, props, state) => {
c.sync(c.createVRoot().child(props.a.toString()));
});
attached
Attached handler will be invoked when component is attached to the document.
const onChange = new Invalidator();
const MyComponent = new ComponentDescriptor<void, void>()
.attached((c, props, state) => {
c.subscribe(onChange);
})
.update((c) => {
c.sync(c.createVRoot().child("content"));
});
onChange.invalidate();
detached
Detached handler will be invoked when component is detached from the document.
const MyComponent = new ComponentDescriptor<void, {onResize: (e: Event) => void}>()
.init((c) => {
c.state = {onResize: (e) => { console.log("window resized"); }};
})
.attached((c, props, state) => {
window.addEventListener("resize", state.onResize);
})
.detached((c, props, state) => {
window.removeEventListener(state.onResize);
})
.update((c) => {
c.sync(c.createVRoot().child("content"));
});
disposed
Disposed handler will be invoked when component is disposed.
let allocatedComponents = 0;
const MyComponent = new ComponentDescriptor<void, void>()
.init((c) => {
allocatedComponents++;
})
.disposed((c) => {
allocatedComponents--;
})
.update((c) => {
c.sync(c.createVRoot().child("content"));
});
newPropsReceived
New props received handler overrides default props received behavior and it should mark component as dirty if new received props will cause change in component's representation.
const MyComponent = new ComponentDescriptor<{a: number}, void>()
.newPropsReceived((c, oldProps, newProps) => {
if (oldProps.a !== newProps.a) {
c.markDirty();
}
})
.update((c) => {
c.sync(c.createVRoot().child(props.a.toString()));
});