forest

UI engine for web

Usage no npm install needed!

<script type="module">
  import forest from 'https://cdn.skypack.dev/forest';
</script>

README

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)
}

Try it

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
    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's value. {value: createStore('initial')} will become "value"="initial"

  • style: add inline styles. All style objects will be merged to single style 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"

  • 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 or stopPropagation 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:

  • 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>}
      }
}): void

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 avoid store.map and remap 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 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 | 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}