statebot-mithril-hooks

Mithril Hooks for Statebot.

Usage no npm install needed!

<script type="module">
  import statebotMithrilHooks from 'https://cdn.skypack.dev/statebot-mithril-hooks';
</script>

README

statebot-mithril-hooks

Mithril Hooks for Statebot.

For React itself, see: statebot-react-hooks

Examples

Installation:

npm i mithril mithril-hooks statebot \
      statebot-mithril-hooks

useStatebot

For hooking-into Statebots that have life-cycles independent of the components that use them, useStatebot:

import m from 'mithril'
import { withHooks as MithrilComponent } from 'mithril-hooks'

import { Statebot } from 'statebot'
import { useStatebot } from 'statebot-mithril-hooks'

export const loadMachine = Statebot('loader', {
  chart: `
    idle ->
      waiting ->
      loaded | failed ->
      idle
  `
})

loadMachine.performTransitions(({ emit }) => ({
  'idle -> waiting': {
    on: 'start-loading',
    then: () => {
      // Fail half the time for this demo
      const fail = Math.random() > 0.5
      setTimeout(() => {
        fail ? emit('error') : emit('success')
      }, 1000)
    }
  },
  'waiting -> loaded': {
    on: 'success'
  },
  'waiting -> failed': {
    on: 'error'
  }
}))

const { Enter, Emit, inState } = loadMachine

const LoadingButton = MithrilComponent(() => {
  const state = useStatebot(loadMachine)

  return m(
    'button',
    {
      className: state,
      onclick: Emit('start-loading'),
      disabled: !inState('idle')
    },
    inState('idle', 'Load'),
    inState('waiting', 'Please wait...'),
    inState('loaded', 'Done!'),
    inState('failed', 'Whoops!')
  )
})

const ResetButton = MithrilComponent(() => {
  return m(
    'button',
    {
      onclick: Enter('idle')
    },
    'Reset'
  )
})

You can play around with this one in a CodeSandbox.

useStatebotFactory

For Statebots whose life-cycles are tied to the components using them, useStatebotFactory:

import m from 'mithril'
import { withHooks } from 'mithril-hooks'
import { useStatebotFactory } from 'statebot-mithril-hooks'

const CHART = `
  idle ->
    loading -> (loaded | failed) ->
    idle
`

const EVENT = {
  START_LOADING: 'start-loading',
  LOAD_SUCCESS: 'load-success',
  LOAD_ERROR: 'load-error'
}

const LoadingButton = withHooks(props => {
  const { state, bot } = useStatebotFactory(
    'loading-button',
    {
      chart: CHART,
      startIn: 'idle',
      logLevel: 4,

      performTransitions: ({ Emit }) => ({
        'idle -> loading': {
          on: EVENT.START_LOADING,
          then: () => setTimeout(
            Emit(EVENT.LOAD_SUCCESS),
            1000
          )
        },
        'loading -> loaded': {
          on: EVENT.LOAD_SUCCESS
        },
        'loading -> failed': {
          on: EVENT.LOAD_ERROR
        }
      }),

      onTransitions: () => ({
        'loading -> failed': () => {
          console.log('Oops...')
        }
      })
    }
  )

  return (
    <button
      className={state}
      onClick={bot.Emit(EVENT.START_LOADING)}
      disabled={bot.inState('loading')}
    >
      {bot.inState('idle', 'Load')}
      {bot.inState('loading', 'Please wait...')}
      {bot.inState('loaded', 'Done!')} ({state})
    </button>
  )
})

useStatebotEvent

To hook-into onEvent, onEntering/ed, onExiting/ed, onSwitching/ed with side-effects cleanup, useStatebotEvent:

import m from 'mithril'
import { withHooks } from 'mithril-hooks'
import { Statebot } from 'statebot'
import {
  useStatebot,
  useStatebotEvent
} from 'statebot-mithril-hooks'

const bot = Statebot('loader', {
  chart: `
    idle ->
      loading -> (loaded | failed) ->
      idle
  `
})

const { Enter, Emit, inState } = bot

const LoadingButton = withHooks(props => {
  const state = useStatebot(bot)

  useStatebotEvent(bot, 'onEntered', 'loading', () =>
    setTimeout(
      bot.Emit(EVENT.LOAD_SUCCESS),
      seconds(1)
    )
  )

  // You can achieve the same with useEffect, and you
  // get more control over the dependencies, too:
  useEffect(() => {
    const cleanupFn = bot.onExited('loading', () =>
      setTimeout(
        bot.Enter('idle'),
        seconds(2)
      )
    )
    return cleanupFn
  }, [bot])

  return (
    <button
      className={state}
      onClick={Emit('start-loading')}
      disabled={inState('loading')}
    >
      {inState('idle', 'Load')}
      {inState('loading', 'Please wait...')}
      {inState('loaded', 'Done!')} ({state})
    </button>
  )
})

function seconds(n) {
  return n * 1000
}

Notes

As you can see, the examples use JSX, which is not always typical of a Mithril project.

Here are some of the config settings for getting this working, if you're interested (lifted from the Mithril docs):

npm i --save-dev @babel/plugin-transform-react-jsx
// .babelrc
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
        "pragma": "m",
        "pragmaFrag": "'['"
    }]
  ]
}

npm i --save-dev eslint-plugin-mithril
// .eslintrc
{
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "extends": ["plugin:mithril/recommended"]
}

useRef

There's no useRef hook provided by mithril-hooks, but the following technique works for me:

import m from 'mithril'
import { withHooks, useState } from 'mithril-hooks'

const ContainerWithRef = withHooks(props => {
  const { children, setRef = () => {} } = props || {}
  return (
    <div oncreate={vnode => setRef(vnode.dom)}>
      {children}
    </div>
  )
})

// Later...

const MyComponentThatNeedsAnElementRef = withHooks(props => {
  const { children } = props || {}
  const [elementRef, setElementRef] = useState()

  useEffect(() => {
    console.log('elementRef = ', elementRef)
  }, [elementRef])

  return (
    <>
      <ContainerWithRef setRef={setElementRef}>
        {children}
      </ContainerWithRef>
    </>
  )
})

Contributing

This is a pretty basic implementation of hooks for Statebot. I don't think much else is needed, but by all means fork and tinker with it as you like.

Of course, please stop-by the Statebot repo itself. :)

License

Statebot was written by Conan Theobald and is MIT licensed.