
kaiju is a view layer used to build an efficient tree of stateless/stateful components and help you manage that tree data.
class / this nonsensekaiju adds the concept of encapsulated components to snabbdom's pure functional virtual dom.
Standard Virtual nodes and components are composed to build a VNode tree that can scale in size and complexity.
A VNode is what you get when calling snabbdom's h function for instance.
A component is simply a function that takes an option object as an argument and returns a VNode ready to be used inside its parent children, i.e, this is a valid array of VNodes:
[
h('div'),
myComponent({ someProp: 33 }),
h('p', 'hello')
]
Note: typescript will be used in the examples, however the library also works just fine with javascript.
0) We start with a stateless "component"
function button() {
return h('button')
}
1) For comparison sake, here is the simplest stateful component definition one can write:
import { Component, h } from 'kaiju'
export default function button() {
return Component({ name: 'button', initState, connect, render })
}
function initState() { return {} }
function connect() {}
function render() {
return h('button')
}
Now, that isn't terribly useful because we really want our component to be stateful, else we would just use a regular VNode object.
2) Let's add some state, and make it change over time:
import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'
export default function() {
return Component<{}, State>({ name: 'button', initState, connect, render })
}
interface State {
text: string
}
function initState() {
return { text: '' }
}
const click = Message('click')
function connect({ on }: ConnectParams<{}, State>) {
on(click, () => ({ text: 'clicked' }))
}
function render({ state }: RenderParams<{}, State>) {
return h('button', { events: { click } }, state.text)
}
Now we created a Message named click that is locally sent to our component whenever the user clicks on the button.
We handle that message in connect and return the new state of our component. The component will then redraw with that new state.
Using explicit Messages instead of callbacks to update our state brings consistency with other kinds of (external) state management and makes state debugging easier since messages can be traced and logged (see logging).
In the above code, on(click) is in fact a shortcut for on(msg.listen(click)).
Here's the longer form:
function connect({ on, msg }: ConnectParams<{}, State>) {
on(msg.listen(click), () => ({ text: 'clicked' }))
}
What msg.listen(click) returns is an Observable that emits new values (the payload of each message)
every time the message is sent.
This is very useful because observables can easily be composed:
function connect({ on, msg }: ConnectParams<{}, State>) {
const clicks = msg.listen(click).debounce(1000)
on(clicks, () => ({ text: 'clicked' }))
}
Now, the state is only updated if we stopped clicking for 1 second.
We could also decide to just perform a side effect, instead of updating the component's state. When performing side effects (void/undefined is returned) the component is not redrawn:
function connect({ on, msg }: ConnectParams<{}, State>) {
const clicks = msg.listen(click).debounce(1000)
on(clicks, _ => console.log('clicked!'))
}
Our component now has an internal state and we know how to update it. But it's also completely opaque from the outside!
In a tree of VNodes, parents must often be able to influence the rendering of their children.
3) For that purpose, we introduce props:
import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'
export default function(props: Props) {
return Component<Props, State>({ name: 'button', props, initState, connect, render })
}
interface Props {
defaultText: string
paragraph: string
}
interface State {
text: string
}
function initState(initProps: Props) {
return { text: initProps.defaultText }
}
const click = Message('click')
function connect({ on }: ConnectParams<Props, State>) {
on(click, () => ({ text: 'clicked' }))
}
function render({ props, state }: RenderParams<Props, State>) {
return (
h('div', [
h('button', { events: { click } }, state.text),
h('p', props.paragraph)
])
)
}
Now our parent can render the component with more control: It can set the default text that should be displayed initially, but also
directly sets the paragraph text of the p tag.
When composing components, you must choose which component should own which piece of state. Disregarding global state for now, local state can reside in a component or any of its parent hierarchy.
4) Let's see how we can move the previous button text state one level up, so that the component parent can directly change that state:
import { Component, h, Message, ConnectParams, RenderParams } from 'kaiju'
export default function(props: Props) {
return Component<Props, {}>({ name: 'button', props, initState, connect, render })
}
interface Props {
text: string
paragraph: string
onClick: Message.OnePayload<Event>
}
function initState() {
return {}
}
const click = Message('click')
function connect({ on, props, msg }: ConnectParams<Props, State>) {
on(click, event => msg.sendToParent(props().onClick(event)))
}
function render({ props, state }: RenderParams<Props, {}>) {
return (
h('div', [
h('button', { events: { click } }, props.text),
h('p', props.paragraph)
])
)
}
We now delegate and send a message to our direct parent component so that it can, in turn, listen to that message from its connect function and update its own state.
Note: The child component could send the same Message to its parent (delegation) but we choose to go with an explicit onClick property to increase semantics, cohesion and typesafety.
At this point, the component is no longer stateful and providing it didn't have any other state, should probably be refactored back to a simple function returning a VNode:
interface Props {
text: string
paragraph: string
onClick: Message.OnePayload<Event>
}
function button(props: Props) {
const { text, paragraph, onClick } = props
return (
h('div', [
h('button', { events: { click: onClick } }, text),
h('p', paragraph)
])
)
}
Finally, if we wanted a generic component we could declare it like so:
export default function<T>(props: Props<T>) {
return select(props)
}
type Props<T> = {
items: T[]
selectedItem: T
onChange: Message.OnePayload<T>
}
type State = {
focusedIndex: number | undefined
}
const select = (function<T>() {
function initState() {
return { focusedIndex: undefined }
}
function connect({}: ConnectParams<Props<T>, State>) {}
function render({}: RenderParams<Props<T>, State>) {
return h('ul')
}
return function(props: Props<T>) {
return Component<Props<T>, State>({ name: 'select', props, initState, connect, render })
}
})()
kaiju comes with an implementation of observables (also known as streams) so that components can more easily declare
how their state should change based on user input and any other observable changes in the application.
Observables are completely optional: If you are more confident with just sending messages around every time the state should update, you can do that too.
The characteristics of this observable implementation are:
To see observables in action, check the example's ajax abstraction and its usage
import { Observable } from 'kaiju'
const obs = Observable.pure(100).map(x => x * 2).delay(200)
The Observable OO API:
interface Observable<T> {
/**
* Subscribes to this observable values and returns a function that may be used to unsubscribe.
*/
subscribe: (onValue: (val: T) => void) => () => void | void
/**
* Gives a debug name to the observable.
* names are inherited by observable transforms unless a new name is defined downstream.
*/
named: (name: string) => this
/**
* Reads the current value of the observable or returns undefined if no value was ever pushed in the observable.
*/
(): T | undefined
/**
* Delays values until a certain amount of silence has passed.
* Values in between silence periods are discarded.
*/
debounce(wait: number): Observable<T>
/**
* Delays all values by a fixed time offset
*/
delay(delay: number): Observable<T>
/**
* Creates a new Observable with adjacent repeated values removed.
* A compare function can optionally be passed to implement user-defined equality instead of strict reference equality.
* The functions should return true if the two values are equal.
*/
distinct(compareFunction?: (previousValue: T, currentValue: T) => boolean): Observable<T>
/**
* Drops 'count' initial values.
* Note: This can also be used to drop the initial value when subscribing to an Observable, if it had seen a value previously.
*/
drop(count: number): Observable<T>
/**
* Filters the values of this observable.
*/
filter<T>(predicate: (t: T) => boolean): Observable<T>
/*
* Maps and flattens this observable then only publish values from the observable mapped last.
*/
flatMapLatest<B>(mapFn: (t: T) => Observable<B>): Observable<B>
/**
* Maps the values of this observable.
*/
map<B>(mapFn: (t: T) => B): Observable<B>
/**
* Partitions this observable into two observables based on a predicate
*/
partition<T>(predicate: (value: T) => boolean): [Observable<T>, Observable<T>]
/**
* Groups values in fixed size blocks (of size 2) by passing a "sliding window" over them.
* An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
* then a size of 2 for subsequent values.
* The newest value is always found at the index 0 of the Array for convenience and type safety.
*/
sliding2(): Observable<[T, T | undefined]>
/**
* Groups values in fixed size blocks by passing a "sliding window" over them.
* An array becomes the value of the new observable. It will have a size of 1 the first time a value is produced,
* then an increasing size of max (maxWindowSize parameter) for subsequent values.
* The newest value is always found at the index 0 of the Array for convenience (...rest parameters).
*/
sliding(maxWindowSize: number): Observable<Array<T>>
/**
* Delays values so that values are produced at most once per every 'time' milliseconds
*/
throttle(time: number): Observable<T>
}
The Observable static API:
```ts
interface ObservableObject { /* Creates a new Observable / (activate?: (ad
$ claude mcp add kaiju \
-- python -m otcore.mcp_server <graph>