Alm

This is a website for Alm, a neat web application framework I made. The README on GitHub has a lot of information. You can also see a more involved, obligatory todo list app as well as its code.

Alm's purpose is to help organize your web applications in a maintainable, modular, and safe way. This page is a gallery of simple examples showcasing the main features and principles of Alm.

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.

An App asks for the following right up front:

  • Your application's initial state;
  • How to take an action and a state to update it;
  • How to transform browser events into actions (main);
  • How to render the view given a state;
  • Where in the document hierarchy to listen for events and render the view.

Your application state is kept inside the App. In main you specify how to turn signals of events into actions, which you then send to the special scope.actions signal.

Actions are then forwarded to your update function, where you are given both the action and the old state. Your task is to return a new state.

Your state is then passed to your render function to produce a virtual DOM. A fancy algorithm turns this into a set of changes to make to the real DOM and then your view is updated.

Signals are how data flows through your app.

                        
/* A simple counter */
const el = alm.el;
const counterApp = new alm.App({
    state: 0,
    update: (action, num) => num + (action ? 1 : -1),
    main: scope => {
        scope.events.click
            .filter(evt => evt.getId() === 'up-btn')
            .recv(evt => scope.actions.send(true));

        scope.events.click
            .filter(evt => evt.getId() === 'down-btn')
            .recv(evt => scope.actions.send(false));
    },
    render: state =>
        el('div', { 'id':'app-1-main' }, [
            el('h3', { 'class':'app-1-header' }, [state.toString()]),
            el('span', {}, [
                el('button', { 'id':'up-btn' }, ['+1']),
                el('button', { 'id':'down-btn' }, ['-1'])
            ])
        ]),
    eventRoot: 'counter-app',
    domRoot: 'counter-app'
}).start();
                        
                    

Events!

Events are communicated via signals.

When you create your App a number of default event signals are created and given to you via the scope argument to your main function. When your app finally starts up only those events you actually subscribed to will actually be registered.

The actual values emitted wrap around raw browser events and provide some simple convenience methods - #getId, #getValue, and #hasClass, all of which provide information about the event target. And the raw event is only a call to #getRaw away.


const eventApp = new alm.App({
    state: { count: 0, overLimit: false },
    update: (text, state) => {
        state.count = text.length;
        state.overLimit = state.count > 140;
        return state;
    },
    main: scope => {
        scope.events.input
            .filter(evt => evt.getId() === 'text-event')
            .recv(evt => scope.actions.send(evt.getValue()));
    },
    render: state =>
        el('div', {}, [
            el('textarea', { 'id': 'text-event' }),
            el('p', {
                'id':'limit-text',
                'class': state.overLimit ? 'warning' : ''
            }, [state.count.toString() + ' / 140 characters'])
        ]),
    eventRoot: 'event-app',
    domRoot: 'event-app'
}).start();
                    

A colorful port example

You can communicate outside your App using port signals. Ports are specified with the ports property. You can provide an array of names, or you can provide an object whose keys are themselves arrays of port names. Eg,


ports: ['port1','port2']
                        
or

ports: {
  outbound: ['out1'],
  inbound: ['in1','in2']
}
                        

The ports created by your app are part of the object returned by App#start. As with other signals you can either send them values or recv values from them.


const colorApp = new alm.App({
    state: '#ffffff',
    update: (value, color) => value,
    ports: ['background'],
    main: scope => {
        scope.events.input
            .filter(evt => evt.getId() === 'app2-color')
            .recv(evt => scope.actions.send(evt.getValue()));

        scope.events.click
            .filter(evt => evt.getId() === 'app2-reset')
            .recv(_ => scope.actions.send('#ffffff'));

        scope.state.connect(scope.ports.background);
    },
    render: color =>
        el('span', {}, [
            el('input', { 'type':'color',
                          'id':'app2-color',
                          'value':color }),
            el('button', { 'id':'app2-reset' }, ['Reset'])
        ]),
    eventRoot: 'color-app',
    domRoot: 'color-app'
}).start();

colorApp.ports.background.recv(color => {
    document.body.style.backgroundColor = color;
});
                    

A quick word on Signals

You don't need to know a whole lot about Signals to use Alm effectively but some things are worth mentioning until I can get some source documentation up.

Signals have some straightforward methods

as well as

The #map, #filter, #connect, and #reduce methods all return different signals than the one whose method you called. This allows Alm signal wiring to be very declarative.

There are certainly more examples to come but this should be a pretty good start to understanding what Alm is all about.

Check out GitHub repository for instructions on how to get, use, or rebuild Alm. If you have any questions or comments don't hesitate to email me.