Alm

This is a website for Alm, an application lifecycle manager written in TypeScript. It is heavily inspired by React, Redux, and Elm.

You can view an obligatory todo list application as well as its code to see Alm in action.

Alm enforces a reactive application design. Reactive applications can be thought of as essentially a pair of functions: one which accepts events and produces updated state; and one which accepts a state and produces a view.

If you have any questions or comments or free money don't hesitate to reach me at gatlin@niltag.net.

Getting started: a simple counter.

Alm applications accept an initial state model, an update function, and a view functon.

In this example the model is a number, initially 0. There are two actions we can perform: Increment or Decrement.

The update function is straightforward:


(state, action) =>
    action.type === CounterActions.Increment
        ? state + 1
        : state - 1
                        
If the type of the message is CounterActions.Increment then add 1 to the state; otherwise, subtract 1.

The view is defined by creating a Component, a function which accepts some object of data properties and returns a View.

A Component does not specify or know how its properties are set. To connect it to the application state the connect function is used.

connect accepts two functions:

  1. A function of the type state -> properties; and
  2. A function of the type (message -> state) -> properties, giving a Component a way to dispatch events.
The result is a new Component.

Alm exports the convenience function el to construct a Virtual DOM easily.

                        
enum CounterActions {
    Increment,
    Decrement
};

const CounterComponent = ({ counter, increment, decrement }) =>
    el('div', {}, [
        el('p', {}, [counter.toString()]),
        el('div', {}, [
            el('button', { on: { click: evt => increment() } },
                ['Increment']),
            el('button', { on: { click: evt => decrement() } },
                ['Decrement'])
        ])
    ]);

const CounterView = connect(
    counter => ({ counter }),
    dispatch => ({
        increment: () => dispatch({ type: CounterActions.Increment }),
        decrement: () => dispatch({ type: CounterActions.Decrement })
    })
)(CounterComponent);

const counterApp = new Alm({
    model: 0,
    update: (state, action) => action.type === CounterActions.Increment
        ? state + 1
        : state - 1,
    view: CounterView(),
    domRoot: 'counter-app',
    eventRoot: 'counter-app'
});

counterApp.start();
                        
                    

The virtual DOM and event handling

In Alm the user interface is described using a virtual DOM. A virtual DOM is a lightweight imitation of the actual browser DOM which can be created and manipulated much more efficiently than the real thing.

Each time the state is updated the virtual DOM is created, compared to the previous one, and the real DOM is manipulated automatically. The result is declarative, straightforward code.

The el function produces a virtual DOM and accepts three arguments:

  1. A string consisting of the XML element, or a Component function;
  2. An attribute object for the element (or a property object for the Component); and
  3. An optional array of children.

The special attribute on lets you specify handlers for browser events. A handler accepts an AlmEvent and can do whatever it wants with it. Frequently the event will be used to dispatch a message.

A Component has access to whatever properties it is given. The connect function gives a Component access to both the application's state as well as a dispatch function. dispatch delivers messages to the application to update the state.

The Message type is defined like so:

                        
type Message<T> = { 'type': T;  data?: any; };
                        
                        
The T type parameter specifies the different actions a Message may represent. The optional data property allows arbitrary data to be sent with each message.


type EventState = {
    count: number;
    overLimit: false;
    inputText: string;
};

enum EventActions {
    UpdateText
};

const eventReducer = (state, action) => {
    switch (action.type) {
        case EventActions.UpdateText: {
            let inputText = action.data;
            return {
                inputText,
                count: inputText.length,
                overLimit: inputText.length > 140
            };
        }
        default:
            return state;
    };
};

const EventComponent = ({ inputText, count, overLimit, updateText }) =>
    el('div', {}, [
        el('textarea', {
            id: 'text-event',
            on: {
                input: evt => updateText(evt.getValue())
            }
        }),
        el('p', {
            'class': overLimit ? 'warning ' : ''
        }, [count.toString() + ' / 140 characters'])
    ]);

const EventView = connect(
    state => state,
    dispatch => ({
        updateText: data => dispatch({
            type: EventActions.UpdateText,
            data
        })
    })
)(EventComponent);

const eventApp = new Alm({
    model: { inputText: '', count: 0, overLimit: false },
    update: eventReducer,
    view: EventView(),
    eventRoot: 'event-app',
    domRoot: 'event-app'
});

eventApp.start();
                    

Asynchronous Events!

A Component is not the only place where your application can send messages. Frequently it is desirable for one event to trigger another. An Alm application accepts a second type of asynchronous message.


type AsyncMessage<S, A> =
    (d: (a: Message<A>) => void, s: () => S)
    => Message<A>;
                        
An AsyncMessage is a function accepting a dispatch function and a function that produces the application state, and then yields a new Message.

In the example requestPageAction is such a message. It constructs an XMLHttpRequest, immediately returns a message stating that a request is in progress, and sets up another message to be fired when the data is loaded.

The update function asyncReducer is very straightforward, as is the AsyncComponent. In a non-trivial application it can become cumbersome to keep track of state or follow the flow of data. A reactive application is an arrow from input events to an output view, which can generate new events in the process. It is immediately obvious where events come from and where they go.


enum AsyncActions {
    RequestPage,
    SetPageText,
    SetPageUrl
};

const setPageUrlAction = data => ({
    type: AsyncActions.SetPageUrl,
    data
});

const setPageTextAction = data => ({
    type: AsyncActions.SetPageText,
    data
});

const requestPageAction = () => (dispatch, state) => {
    const r = new XMLHttpRequest();
    r.open("GET", state().pageUrl, true);
    r.onreadystatechange = () => {
        if (r.readyState !== 4 || r.status !== 200) {
            return;
        }
        dispatch(setPageTextAction(r.responseText));
    };
    r.send();
    return {
        type: AsyncActions.RequestPage
    };
};

const asyncReducer = (state, action) => {
    switch (action.type) {
        case AsyncActions.RequestPage:
            return { ...state, requesting: true };
        case AsyncActions.SetPageText:
            return { ...state, requesting: false, pageText: action.data };
        case AsyncActions.SetPageUrl:
            return { ...state, pageUrl: action.data };
        default:
            return state;
    }
};

const AsyncComponent = props =>
    el('div', {}, [
        el('h3', {}, ["Load web page"]),
        el('input', {
            type: 'text',
            value: props.pageUrl,
            on: {
                input: evt => props.setPageUrl(evt.getValue())
            }
        }),
        el('button', {
            on: {
                click: evt => props.requestPage()
            }
        }, ['Load Page']),
        el('p', {}, [props.requesting
            ? 'Loading ...'
            : 'Number of characters received: ' + props.pageText.length
        ])
    ]);

const AsyncView = connect(
    state => state,
    dispatch => ({
        setPageUrl: url => dispatch(setPageUrlAction(url)),
        requestPage: () => dispatch(requestPageAction()),
        setPageText: text => dispatch(setPageTextAction(text))
    })
)(AsyncComponent);

const asyncApp = new Alm({
    model: { pageText: '', requesting: false, pageUrl: 'http://niltag.net' },
    update: asyncReducer,
    view: AsyncView(),
    eventRoot: 'async-app',
    domRoot: 'async-app'
});

asyncApp.start();