@sergeysova/forest v1.0.0
forest
UI engine for web
Usage
import {createStore, createEvent, sample} from 'effector'
import {using, spec, h} from 'forest'
using(document.body, () => {
const {change, submit, state} = 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'},
handler: {input: change('password')},
})
h('button', {
text: 'Submit',
attr: {
disabled: state.map(values => !(values.username && values.password)),
},
})
})
h('section', () => {
spec({style: {marginTop: '1em'}})
h('div', {text: 'Reactive form debug:'})
h('pre', {text: state.map(stringify)})
})
})
})
function formModel() {
const state = createStore({})
const changed = createEvent()
const submit = createEvent()
state.on(changed, (data, {name, value}) => ({...data, [name]: value}))
const change = name => changed.prepend(e => ({name, value: e.target.value}))
sample({
source: state,
clock: submit,
fn: stringify,
}).watch(alert)
return {change, submit, state}
}
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
},
): voidh
Declare single dom element.
function h(tag: string, fn: () => void): void
function h(
tag: string,
config: {
attr?: PropertyMap
style?: PropertyMap
styleVar?: PropertyMap
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
},
): voidSee also: PropertyMap, Property
Config fields:
attr: add HTML attributes, e.g.
classor input'svalue.{value: createStore('initial')}will become"value"="initial"style: add inline styles. All
styleobjects will be merged to singlestylehtml 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"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 renderinghandler: add event handlers to dom node. In cases when
preventDefaultorstopPropagationis 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
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>}
}
}): voidlist
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
}): voidConfig 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
fieldsas 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
fnas array of separate stores. Useful to avoidstore.mapandremapcalls
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}
}): voidroute
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
}): voidtext
Use template literals to add text to dom node. Accept any properties
function text(words: TemplateStringsArray, ...values: Property[]): voidExample
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>}) => voidblock
Allow defining and validate template outside from using calls.
function block(config: {fn: () => void}): () => voidrenderStatic
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[]): stringExample
const x = createStore(10)
const y = 20
h('g', {
attr: {
transform: val`translate(${x} ${y})`,
},
})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 | booleanProperty
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}4 years ago