Nearly Perfected Mystique

    forest
    TypeScript icon, indicating that this package has built-in type declarations

    0.21.1 • Public • Published

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

    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
        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'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"

    • 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 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
      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 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 $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: { classList: ['class-name', $stringStore] } })

    Install

    npm i forest

    Homepage

    effector.dev

    DownloadsWeekly Downloads

    3,245

    Version

    0.21.1

    License

    MIT

    Unpacked Size

    664 kB

    Total Files

    18

    Last publish

    Collaborators

    • drelliot
    • sergeysova
    • lobatik
    • zero_bias