Fast Virtual DOM with Reactive updating.
- Fast thanks to snabbdom, most, component isolation and async RAF rendering
- Global and local states use streams for greater composition
- No JS
class
/this
nonsense - Tiny size in KB
- Comes with useful logs
- Very typesafe / typescript friendly
dompteuse
adds the concept of encapsulated components to pure functional virtual dom.
Standard Virtual nodes and components are composed to build a Vnode tree that can scale in size and complexity.
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.
Note: typescript will be used in the examples, javascript devs can simply ignore the types annotations.
import { Component } from 'dompteuse'
export default function(props?: Props) {
return Component({
key: 'Select',
props,
defaultProps,
initState,
connect,
render
})
}
Let's look at the option object properties:
Mandatory String
This is the standard Virtual node key used to uniquely identify this Vnode. It is also used for logging purposes, so it is usually just the name of the component.
Optional Object
An object representing all the properties passed by our parent.
Typically props either represent state that is maintained outside the component or properties used to tweak the component behavior.
The render
function will be called if the props object changed shallowly, hence it's a better practice to use a flat object.
Note: props and state are separated exactly like in React
as it works great. The same best practices apply.
Optional Object
(upper type of props)
An object with some keys of the props that should be used if the parent do not specify all the props.
Mandatory Object
A function taking the initial props as an argument and returning the starting state.
Mandatory function(on: StreamSub<State>, events: Events): void
Connects the component to the rest of the app and computes the local state of the component.
connect
is called only once when the component is mounted.
connect
is called with two arguments:
on
registers a Stream that modifies the component local state.
events
is the interface used to listen to bubbling dom events or emit custom Messages.
connect
arguably has more interesting characteristics than the imperative approach React
use (i.e setState
):
-
Streams are composable, callbacks are not. Doing things like throttling or only listening to the very last ajax action fired is a recurrent, non abstractable pain with imperative code.
-
Separation of the markup and the component hierarchy wiring logic.
render functions in react can get pretty messy, having to pass callbacks several level down. What's more, callback references often change over time (most likely from using partial application) and we can no longer apply streamlined performance optimizations because some props truly represent data while other props are callbacks that may or may not purposedly change.
Designers may also feel more at ease with working with a clean tree of Vnodes without having to think about the app logic.
Example:
import { StreamSub, Events, Message } from 'dompteuse'
// A custom, type-safe message used to communicate with our parent hierarchy
export const Opened = Message('opened')
function connect(on: StreamSub<State>, events: Events) {
// Subscribe to the stream of button clicks and update our state every time it changes
on(events.listen('button', 'click'), state => {
const opened = !state.opened
// dispatch a custom message conditionally.
// Any parent component can listen to it using events.listen('css selector', Opened)
if (opened) dom.emit(Opened())
// Any 'on' handler must return the new component state
return merge(state, { opened })
})
}
connect
can listen to any kind of most
stream, not just the provided dom event stream. See global streams.
Just like with props, a redraw will only get scheduled if the state object changed shallowly so returning the current state
in on()
will skip rerendering.
Mandatory function(props: Props, state: State): VNode
Returns the Vnode tree based on the props and state.
Example:
import { h } from 'dompteuse'
interface State {
text: string
}
function render(props: void, state: State) {
const { text } = state
return h('div#text', [
h('h1', 'Hello'),
h('p', text)
])
}
A construct is provided to easily build push-based global streams in a typesafe fashion. This is entirely optional.
You typically want to keep very transient state as local as possible so that it remains encapsulated in a component and do not leak up such as:
- Whether a select dropdown is opened
- The component has focus
- Which grid row is highlighted
- Basically any state that resets if the user navigate away then come back
Additionally, keeping state that is only useful to one screen should be kept inside the top-most component of that screen and no higher.
That leaves global state, which can be updated from anywhere and is read from multiple screens such as:
- The current route
- User preferences
- Any raw domain data that will be mapped/filtered/transformed in the different screens.
The subscription to this global stream is automatically released when the component is unmounted.
Example:
import { Message, GlobalStream } from 'dompteuse'
import merge from './util/obj/merge'
export const setUserName = Message<string>('setUserName')
interface UserState {
name: string
}
const initialState = { name: 'bob' }
// This exports a stream ready to be used in a component's connect function
export default GlobalStream<UserState>(initialState, on => {
on(setUserName, (state, name) =>
merge(state, { name })
)
})
// ...
// Subscribe to it in a component's connect
import userState from './userState'
// Provide an initial value
function initialState() {
return {
userName: userState.value.name
}
}
connect(on: StreamSub<State>, events: Events) {
on(userState, (state, user) => {
// 'Copy' the global user name into our local component state to make it available to `render`
return merge(state, { userName: user.name })
})
}
// ...
// Then anywhere else, import setUserName and use it
setUserName('Monique')
Creates a Vnode
This is proxied to snabbdom's h so we can add our type definitions
transparently.
import { h } from 'dompteuse'
h('div', 'hello')
Performs the initial render of the app synchronously.
function startApp<S>(options: {
app: Vnode // The root Vnode
elm: HTMLElement // The root element where the app will be rendered
patch: PatchFunction // The snabbdom patch function to be used during renders
}): void;
import { snabbdom, startApp } from 'dompteuse'
import app from './app'
declare var require: any
const patch = snabbdom.init([
require('snabbdom/modules/class'),
require('snabbdom/modules/props'),
require('snabbdom/modules/attributes'),
require('snabbdom/modules/style')
])
startApp({ app, patch, elm: document.body })
Create a custom application message used to either communicate between components or push to a GlobalStream.
import { Message } from 'dompteuse'
// Message taking no arguments
const increment = Message('increment')
// Message taking one argument
const incrementBy = Message<number>('incrementBy')
The registration function passed to connect
used to subscribe to a stream and update the component state.
Signature:
on(stream: Stream<A>, handler: (state: State, payload: A) => State)
The api object passed to connect
and used to communicate via events propagating through the DOM.
Detail:
events.listen(selector: string, eventName: string): Stream<Event>
events.listen<P>(selector: string, message: Message<P>): Stream<P>
events.emit<P>(message: MessagePayload<P>): void
Example:
import { StreamSub, Events, Message } from 'dompteuse'
import { Opened } from './someComponent'
const Increment = Message('increment')
connect(on: StreamSub<State>, events: Events) {
// Regular DOM events
const clickStream = events.listen('button', 'click')
// Custom Messages
const openStream = events.listen('.someComponent', Opened)
// Emit a bubbling custom message. Any parent component can listen to it.
events.emit(Increment())
}