UI engine for web
npm install forestUI engine for web
``js
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)
}
`
Start an application from given root dom node. Can accept forked Scope.
Set hydrate: true to reuse root html content (useful for ssr)
`typescript
function using(root: DOMElement, fn: () => void): void
function using(
root: DOMElement,
config: {
fn: () => void
hydrate?: boolean
scope?: Scope
},
): void
`
Declare single dom element.
`typescript
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
handler?:
| {[domEvent: string]: Event
| {
config: {
passive?: boolean
capture?: boolean
prevent?: boolean
stop?: boolean
}
on: {[domEvent: string]: Event
}
fn?: () => void
},
): void
`
> See also: PropertyMap, Property
Config fields:
- attr: add HTML attributes, e.g. class or input's value. {value: createStore('initial')} will"value"="initial"
become
- style: add inline styles. All style objects will be merged to single style html attribute. Object fields in{borderRadius: '3px'}
camel case will be converted to dash-style, e.g. will become "style"="border-radius: 3px".
- styleVar: add css variables to inline styles. {themeColor: createStore('red')} will"style"="--themeColor: red"
become
- classList: add class names to class attribute. {active: true} will become "class"="active"['active', 'disabled']
, will become "class"="active disabled" and so on with Store support.
- data: add data attributes. Object
fields in camel case will be converted to dash-style, e.g. {buttonType: 'outline'} will"data-button-type"="outline"
become and might be queried in css in this way:
`css`
[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 or stopPropagation is needed, extended
form with config object can be used
`typescript
const click = createEvent
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
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
`typescript`
function spec(config: {
attr?: PropertyMap
style?: PropertyMap
styleVar?: PropertyMap
classList?: ClassListMap | ClassListArray
data?: PropertyMap
text?: Property | Property[]
visible?: Store
handler?:
| {[domEvent: string]: Event
| {
config: {
passive?: boolean
capture?: boolean
prevent?: boolean
stop?: boolean
}
on: {[domEvent: string]: Event
}
}): void
#### classList
Property classList has two forms, each optionally reactive:
- object map
`ts`
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.
`ts`
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:
`ts
h('div', {
attr: {class: 'first second'},
classList: ['third'],
fn() {
spec({classList: {fourth: true}})
},
})
// =>
$3
Render array of items from store
`typescript
function list(source: Store, fn: (config: {store: Store, key: Store}) => void): voidfunction list(config: {
source: Store,
key: string
fields?: string[]
fn: (config: {store: Store, key: Store, fields: Store[]}) => 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
avoid store.map and remap calls$3
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)`typescript
function variant(config: {
source: Store
key: string
cases: {
[caseName: string]: ({store: Store}) => void
}
}): void
`$3
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`typescript
function route(config: {
source: Store
visible: ((value: T) => boolean) | Store
fn: (config: {store: Store}) => void
}): void
`$3
Use template literals to add text to dom node. Accept any properties
`typescript
function text(words: TemplateStringsArray, ...values: Property[]): void
`Example
`typescript
const $username = createStore('guest')h('h1', () => {
text
Hello ${$username}!
})
`$3
Provide support for recursive templates. Can be called outside from using calls
`typescript
function rec(config: {store: Store}): (config: {store: Store}) => void
`$3
Allow defining and validate template outside from using calls.
`typescript
function block(config: {fn: () => void}): () => void
`$3
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`typescript
function renderStatic(fn: () => void): Promisefunction renderStatic(config: {scope?: Scope; fn: () => void}): Promise
`$3
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)`typescript
function remap(store: Store, keys: string[]): Store[]function remap(store: Store, key: string): Store
`$3
Helper for joining properties to single string with template literals. If
only plain values are passed, the method returns
string`typescript
function val(words: TemplateStringsArray, ...values: Property[]): Storefunction val(words: TemplateStringsArray, ...values: PlainProperty[]): string
`Example
`typescript
const $store = createStore(10)
const a = 20h('g', {
attr: {
transform: val
translate(${$store} ${a}),
},
})
`Type terms
$3
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.`typescript
type PlainProperty = string | number | null | boolean
`$3
In most cases dom properties can be wrapped in stores, thereby making result value dynamic
`typescript
type Property = PlainProperty | Store
`$3
Object with dom properties, possibly reactive
`typescript
type PropertyMap = {[field: string]: Property}
`$3
Object with class names as keys and boolean values, possibly reactive
`typescript
type ClassListMap = {[className: string]: Store | boolean}
``typescript
spec({
classList: {
'class-name': true,
'class-name-2': $booleanStore,
},
})
`$3
Array with class names, possibly reactive
`typescript
type ClassListArray = Array | string>
``typescript
spec({
classList: ['class-name', $stringStore],
})
``