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.
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:
state -> properties
; and
(message -> state) -> properties
,
giving a Component
a way to
dispatch events.
Component
.
Alm exports the convenience function el
to
construct a Virtual DOM easily.
enum CounterActions {
Increment,
Decrement
};
const CounterComponent = connect(
counter => ({ counter }),
dispatch => ({
increment: () => dispatch({
type: CounterActions.Increment
}),
decrement: () => dispatch({
type: CounterActions.Decrement
})
})
)(({ counter, increment, decrement }) => (
<div>
<p>{ counter.toString() }</p>
<div>
<button
on={{
click: evt => increment()
}}>
Increment
</button>
<button
on={{
click: evt => decrement()
}}>
Decrement
</button>
</div>
</div>
));
const counterApp = new Alm({
model: 0,
update: (state, action) => action.type === CounterActions.Increment
? state + 1
: state - 1,
view: CounterComponent(),
domRoot: 'counter-app',
eventRoot: 'counter-app'
});
counterApp.start();
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.
JSX can be used to create virtual DOM components (as can be seen in the examples). The todo list example code demonstrates how to set up webpack for JSX support.
JSX support is simply a wrapper around a core function
you may use directly. The el
function
produces a virtual DOM and accepts three arguments:
Component
function;
Component
);
and
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.
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 = connect(
state => state,
dispatch => ({
updateText: data => dispatch({
type: EventActions.UpdateText,
data
})
})
)(({ inputText, count, overLimit, updateText }) => (
<div>
<textarea
id="text-event"
on={{
input: evt => updateText(evt.getValue())
}}/>
<p className={ overLimit ? 'warning ' : '' }>
{ count.toString() + ' / 140 characters' }
</p>
</div>
));
const eventApp = new Alm({
model: { inputText: '', count: 0, overLimit: false },
update: eventReducer,
view: EventComponent(),
eventRoot: 'event-app',
domRoot: 'event-app'
});
eventApp.start();
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 requestPageAction = () => (dispatch, state) => (
fetch(state().pageUrl)
.then(response => response.text())
.then(data => dispatch({
type: AsyncActions.SetPageText,
data
}))
.catch(err => { console.error(err) })
);
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 = connect(
state => state,
dispatch => ({
setPageUrl: url => dispatch({
type: AsyncActions.SetPageUrl,
data: url
}),
requestPage: () => dispatch(requestPageAction())
})
)(props => (
<div>
<h3>Load web page</h3>
<input
type="text"
value={ props.pageUrl }
on={{
change: evt => props.setPageUrl(evt.getValue())
}}
/>
<button on={{ click: evt => props.requestPage()}}>
Load Page
</button>
<p>{ props.requesting
? 'Loading ...'
: 'Number of characters received: ' + props.pageText.length
}</p>
</div>
));
const asyncApp = new Alm({
model: { pageText: '', requesting: false, pageUrl: 'http://niltag.net' },
update: asyncReducer,
view: AsyncComponent(),
eventRoot: 'async-app',
domRoot: 'async-app'
});
asyncApp.start();