forest
UI engine for web
Usage
import {createStore, createEvent, sample} from 'effector'
import {using, spec, h} from 'forest'
using(document.body, () => {
const {change, submit, $fields} = formModel()
h('section', () => {
spec({style: {width: '15em'}})
h('form', () => {
spec({
handler: {
config: {prevent: true},
on: {submit},
},
style: {
display: 'flex',
flexDirection: 'column',
},
})
h('input', {
attr: {placeholder: 'Username'},
handler: {input: change('username')},
})
h('input', {
attr: {type: 'password', placeholder: 'Password'},
classList: ['w-full', 'py-2', 'px-4'],
handler: {input: change('password')},
})
h('button', {
text: 'Submit',
attr: {
disabled: $fields.map(
fields => !(fields.username && fields.password),
),
},
})
})
h('section', () => {
spec({style: {marginTop: '1em'}})
h('div', {text: 'Reactive form debug:'})
h('pre', {text: $fields.map(stringify)})
})
})
})
function formModel() {
const changed = createEvent()
const submit = createEvent()
const $fields = createStore({}).on(changed, (fields, {name, value}) => ({
...fields,
[name]: value,
}))
const change = name => changed.prepend(e => ({name, value: e.target.value}))
sample({
source: $fields,
clock: submit,
fn: stringify,
}).watch(alert)
return {change, submit, $fields}
}
function stringify(values) {
return JSON.stringify(values, null, 2)
}
API
using
Start an application from given root dom node. Can accept forked Scope.
Set hydrate: true
to reuse root
html content (useful for ssr)
function using(root: DOMElement, fn: () => void): void
function using(
root: DOMElement,
config: {
fn: () => void
hydrate?: boolean
scope?: Scope
},
): void
h
Declare single dom element.
function h(tag: string, fn: () => void): void
function h(
tag: string,
config: {
attr?: PropertyMap
style?: PropertyMap
styleVar?: PropertyMap
classList?: ClassListMap | ClassListArray
data?: PropertyMap
text?: Property | Property[]
visible?: Store<boolean>
handler?:
| {[domEvent: string]: Event<any>}
| {
config: {
passive?: boolean
capture?: boolean
prevent?: boolean
stop?: boolean
}
on: {[domEvent: string]: Event<any>}
}
fn?: () => void
},
): void
See also: PropertyMap, Property
Config fields:
-
attr: add HTML attributes, e.g.
class
or input'svalue
.{value: createStore('initial')}
will become"value"="initial"
-
style: add inline styles. All
style
objects will be merged to singlestyle
html attribute. Object fields in camel case will be converted to dash-style, e.g.{borderRadius: '3px'}
will become"style"="border-radius: 3px"
. -
styleVar: add css variables to inline styles.
{themeColor: createStore('red')}
will become"style"="--themeColor: red"
-
classList: add class names to
class
attribute.{active: true}
will become"class"="active"
,['active', 'disabled']
will become"class"="active disabled"
and so on withStore
support. -
data: add data attributes. Object fields in camel case will be converted to dash-style, e.g.
{buttonType: 'outline'}
will become"data-button-type"="outline"
and might be queried in css in this way:
[data-button-type='outline'] {
}
-
text: add text to element as property or array of properties
-
visible: node will be presented in dom tree while store value is
true
. Useful for conditional rendering -
handler: add event handlers to dom node. In cases when
preventDefault
orstopPropagation
is needed, extended form with config object can be used
const click = createEvent<MouseEvent>()
h('button', {
text: 'Click me',
handler: {click},
})
h('a', {
text: 'Click me',
handler: {
config: {prevent: true},
on: {click},
},
})
Handler config fields:
- passive: event handler will be defined as passive
- capture: event handler will be defined with
capture: true
- prevent: call
preventDefault()
on trigger- stop: call
stopPropagation()
on trigger
- fn: add children to given element by nesting api methods calls
spec
Add new properties to dom element. Designed to call from h callbacks and has the same fields as
in h(tag, config)
. Can be called as many times as needed
function spec(config: {
attr?: PropertyMap
style?: PropertyMap
styleVar?: PropertyMap
classList?: ClassListMap | ClassListArray
data?: PropertyMap
text?: Property | Property[]
visible?: Store<boolean>
handler?:
| {[domEvent: string]: Event<any>}
| {
config: {
passive?: boolean
capture?: boolean
prevent?: boolean
stop?: boolean
}
on: {[domEvent: string]: Event<any>}
}
}): void
classList
Property classList
has two forms, each optionally reactive:
- object map
const $isEnabled = createStore(true)
spec({classList: {first: true, second: $isEnabled}})
- array list
Be careful, each array item will be treated as a single class name, so it should not have a spaces.
const $class = createStore('active')
spec({classList: ['size-big', $class]})
If spec with classList called twice or more, all enabled classes will be merged in the order of appearance.
Also, classList
will be merged with static class
attribute:
h('div', {
attr: {class: 'first second'},
classList: ['third'],
fn() {
spec({classList: {fourth: true}})
},
})
// => <div class="first second third fourth"></div>
list
Render array of items from store
function list<T>(source: Store<T[]>, fn: (config: {store: Store<T>, key: Store<number>}) => void): void
function list<T>(config: {
source: Store<T[]>,
key: string
fields?: string[]
fn: (config: {store: Store<T>, key: Store<any>, fields: Store<any>[]}) => void): void
}): void
Config fields:
- source: store with an array of items
- key: field name which value will be used as key for given item
-
fn: function which will be used as a template for every list item. Receive item value and item key as stores
and
fields
as array of stores if provided. All fields are strongly typed and inferred from config definition -
fields: array of item field names which will be passed to
fn
as array of separate stores. Useful to avoidstore.map
andremap
calls
variant
Mount one of given cases by selecting a specific one by the current value of the key
field of source
store value.
Type of store
in cases
functions will be inferred from a case type. Optional default case - __
(like
in split)
function variant<T>(config: {
source: Store<T>
key: string
cases: {
[caseName: string]: ({store: Store<T>}) => void
}
}): void
route
Generalized route is a combination of state and visibility status. fn
content will be mounted until visible
called
with source
value will return true
. In case of store in visible
field, content will be mounted while that store
contain true
. variant is shorthand for creating several routes at once
function route<T>(config: {
source: Store<T>
visible: ((value: T) => boolean) | Store<boolean>
fn: (config: {store: Store<T>}) => void
}): void
text
Use template literals to add text to dom node. Accept any properties
function text(words: TemplateStringsArray, ...values: Property[]): void
Example
const $username = createStore('guest')
h('h1', () => {
text`Hello ${$username}!`
})
rec
Provide support for recursive templates. Can be called outside from using calls
function rec<T>(config: {store: Store<T>}): (config: {store: Store<T>}) => void
block
Allow defining and validate template outside from using calls.
function block(config: {fn: () => void}): () => void
renderStatic
Method from forest/server
to render given application to string. Can accept
forked Scope, in which case fn
children must be wrapped
in block to ensure that all units are created before fork call
function renderStatic(fn: () => void): Promise<string>
function renderStatic(config: {scope?: Scope; fn: () => void}): Promise<string>
remap
Helper for retrieving value fields from single store. Shorthand for several store.map(val => val[fieldName])
calls.
Infer types when used with either single key or
with as const
: const [id, name] = remap(user, ['id', 'name'] as const)
function remap<T>(store: Store<T>, keys: string[]): Store<any>[]
function remap<T>(store: Store<T>, key: string): Store<any>
val
Helper for joining properties to single string with template literals. If
only plain values are passed, the method returns string
function val(words: TemplateStringsArray, ...values: Property[]): Store<string>
function val(words: TemplateStringsArray, ...values: PlainProperty[]): string
Example
const $store = createStore(10)
const a = 20
h('g', {
attr: {
transform: val`translate(${$store} ${a})`,
},
})
Type terms
PlainProperty
Value types accepted by methods, which write values to dom properties. Strings are written as is, numbers are converted
to strings, null
and false
mean no value (property deletion), true
is used when the specific property value is not
needed.
type PlainProperty = string | number | null | boolean
Property
In most cases dom properties can be wrapped in stores, thereby making result value dynamic
type Property = PlainProperty | Store<PlainProperty>
PropertyMap
Object with dom properties, possibly reactive
type PropertyMap = {[field: string]: Property}
ClassListMap
Object with class names as keys and boolean values, possibly reactive
type ClassListMap = {[className: string]: Store<boolean> | boolean}
spec({
classList: {
'class-name': true,
'class-name-2': $booleanStore,
},
})
ClassListArray
Array with class names, possibly reactive
type ClassListArray = Array<Store<string> | string>
spec({
classList: ['class-name', $stringStore],
})