5.1.0 • Published 6 years ago

mainapp v5.1.0

Weekly downloads
3
License
ISC
Repository
github
Last release
6 years ago

You are now in control

mainapp is a micro web application framework, where the control flow starts and is never lost. mainapp implements a state container that you actually want to use. Update state without the ceremony.

Getting started

Here is a simple counter application.

import {h, App} from 'mainapp'

App({
  count: 0,

  down ({count}, value) {
    return {
      count: count - value
    }
  },

  up ({count}, value) {
    return {
      count: count + value
    }
  },

  view ({count, down, up}) {
    return <div>
      <h1>{count}</h1>
      <button onclick={() => down(1)}>-</button>
      <button onclick={() => up(1)}>+</button>
    </div>
  }
}, document.getElementById('mainapp-entry'))

mainapp is compatible with picostyle.

Main loop

All webapps have the same main loop. Let's learn how mainapp implements the main loop.

State tree

The state tree is the whole application. The state tree has both the data and code (views and actions) in the same place. This mean the application can rewrite itself, which is useful for code splitting.

Views

When state is updated, views functions are used to render the application. They return DOM fragments based on the current state tree.

Actions

Actions are functions on the state tree that implement application logic and update the state tree. They are triggered by events or other actions. Actions take the current state tree and return an update. Actions can be asynchronous.

Updating the state tree

Let's take a closer look at the counter sample and show how the state tree changes.

The initial state tree is...

{
  count: 0,
  down ({count}, value) {
    return {
      count: count - value
    }
  },
  up ({count}, value) {
    return {
      count: count + value
    }
  },
  view ({count, down, up}) {
    return <div>
      <h1>{count}</h1>
      <button onclick={() => down(1)}>-</button>
      <button onclick={() => up(1)}>+</button>
    </div>
  }
}

mainapp uses view to render user interface. view receives the current state tree as the first argument into the function. We destructure it to get the current count and two actions. The initial DOM would be...

<div>
  <h1>0</h1>
  <button onclick="() => down(1)">-</button>
  <button onclick="() => up(1)">+</button>
</div>

Let's say the user clicks on the + button. First of all, notice how mainapp uses the native onclick event handler and how its bound to the up action. Looking at the implementation of the up action, we notice similar to the view, the first argument is the current state tree. But wait, in the event handler, we don't pass in the state tree? This is the magic the makes mainapp easy to use. When defining actions we get access to the state tree, but when calling the action, we don't need to worry about it. This is an example of partial application in action.

When the up action is called, it returns {count: 0 + 1}. We don't need to return the whole state tree, we just return what we want to update, in this case the count. This will update the state tree to be...

{
  count: 1,
  ...rest of state tree is unchanged
}

mainapp automatically renders the user interface after the state tree is updated.

Components

mainapp supports components as substate trees. For example, we can extract Counter as a component...

import {h, App} from 'mainapp'

const Counter = {
  count: 0,
  down ({count}, value) {
    return {
      count: count - value
    }
  },
  up ({count}, value) {
    return {
      count: count + value
    }
  },
  view ({count, down, up}) {
    return <div>
      <h1>{count}</h1>
      <button onclick={() => down(1)}>-</button>
      <button onclick={() => up(1)}>+</button>
    </div>
  }
}

App({
  Counter,
  view({Counter}) {
    return <Counter/>
  }
}, document.getElementById('mainapp-entry'))

Each component has their own state. We use the Counter component multiple times and each will have their own count. Notice how the root view renders the Counter using <Counter/>.

Lifecycles

Components can declare lifecycle actions didMount and willUnmount.

  • didMount(state, parentKey): an action on a child component which is called once after mounted on the parent. The parentKey is the key on the parent where the child component was mounted. Use this lifecycle to do one time setup with external APIs.
  • willUnmount(state, parentKey): an action on a child component which is called once before unmounted from the parent. The parentKey is the key on the parent where the child component was mounted. Use this lifecycle to clean up things setup with didMount. willUnmount will be called transitively the entire component sub-tree if ancestor is umounted.

Here is an example on how to use didMount and willUnmount to attach and clean up event listeners.

const {mountOnline, unmountOnline} = App({
  mountOnline() {
    return {
      Online: {
        online: 'unknown',
        wentOnline() {
          return {
            online: 'yes'
          }
        },
        didMount({wentOnline}) {
          window.addEventListener('online', wentOnline)
        },
        willUnmount({wentOnline}) {
          window.removeEventListener('online', wentOnline)
        },
        view({online}) {
          return <p>Online: {online}</p>
        }
      },
    }
  },
  unmountOnline() {
    return {
      Online: undefined
    }
  },
  view({Online}) {
    return {Online && <Online/>}
  }
}, document.getElementById('mainapp-entry'))

await mountOnline()
// didMount fires

await unmountOnline()
// willUnmount fires

Inversion of control

Applications are simpler when the control flow is clear. When the control flow is spread all over the application, it is much harder to change. mainapp allows you to keep control at the root state tree.

While its easy for parent nodes in the state tree to call actions on children nodes, we need a way for child nodes to call actions on their parent or even on the top of the state tree. While this seems dangerous, in practise all webapps need to do this.

Parent and global access

All components have access to their parent state tree via $parent and the global state tree via $global.

import {h, App} from 'mainapp'

const Grandchild = {
  name: 'grandchild',
  view({$global: {name: rootName}, $parent: {name: parentName}, name}) {
    return <div>
      <p>Root name: {rootName}</p>
      <p>Parent name: {parentName}</p>
      <p>Own name: {name}</p>
    </div>
  }
}

const Child = {
  name: 'child',
  Grandchild,
  view({Grandchild}) {
    return <Grandchild/>
  }
}

App({
  name: 'root',
  Child,
  view({Child}) {
    return <Child/>
  }
}, document.getElementById('mainapp-entry'))

This will render...

<div>
  <p>Root name: root</p>
  <p>Parent name: child</p>
  <p>Own name: grandchild</p>
</div>

Component actions can also update $global and $parent.

More samples

5.1.0

6 years ago

5.0.0

6 years ago

4.0.0

6 years ago

3.0.1

6 years ago

3.0.0

6 years ago

2.0.0

6 years ago

1.1.0

6 years ago

1.0.0

6 years ago