Funnel
A purely functional frontend framework based on functional reactive
programming. Experimental.

Ideas/features
The goal of Funnel is to be a powerful framework for building frontend
applications in a purely functional way. Funnel is based on FRP and is
heavily inspired by functional techniques found in Haskell.
- Purely functional.
- Implemented in TypeScript. Later on we'd like to support PureScript
as well.
- Based on classic FRP. Behaviors represents values that changes over
time and streams provide reactivity. Funnel uses the FRP
library Hareactive.
- A component-based architecture. Components are encapsulated and
composable. Components are monads and are typically used and
composed with do-notation (do-notation is implemented with
generators).
- Constructed DOM elements reacts directly to behaviors and streams.
This avoids the overhead of using virtual DOM and should lead to
great performance.
- Side-effects are expressed with a declarative IO-like monad. This
allows for easy testing of effectful code. Furthermore, the IO-monad
is integrated with FRP. This makes it possible to perform
side-effects in response to user input.
- The entire dataflow through applications is explicit and easy to
follow.
High level overview
FRP as building blocks
Funnel builds on top of the FRP library Hareactive. Two of the key
concepts from FRP are:
Behavior
— Represents values that change over time.Stream
— Represents discrete events that happen over time.
They are documented in more detail in
the Hareactive readme.
What is Component
On top of the FRP primitives Funnel adds Component
. Components can
contain logic expressed through combinations of behaviors and streams.
They can run IO-actions and add elements to the DOM.

Components in Funnel are encapsulated. They can have completely
private state and selectively decide what output they deliver to their
parent.
A component
represents one or more DOM elements and the output they produce. For
example, a Component
that represents an input
element can be
created like this
const inputComponent = input();
The component has the type Component<Output>
where Output
is an
object containing the output that an input
element produces. Among
other things an input
element produces a string-valued behavior with
the current content of the input
element and a stream of keyboard
events from the element.
Components are composable and combine into components. A Funnel app
is just one big component. There is no difference between a top level
component and child components. Components combine with their chain
method. The signature of chain
is
chain((output: Output) => Component<NewOutput>): Component<NewOutput>;
Example.
input().chain((inputOutput) => span(inputOutput.inputValue));
An invocation component.chain(fn)
works like this:
- The output from
component
is passed to fn
. fn
returns a new component, let's call it component2
- The DOM-elements from
component
and component2
are concatenated. - The result of the computation is a component with the concatenated
DOM-elements and output equal to the output from
component2
.
So, the above example boils down to this:
Create input component Create span component with text content
↓ ↓
input().chain((inputOutput) => span(inputOutput.inputValue));
↑ ↑
Output from input-element Behavior of text in input-element
The result is an input element followed by a span element. When
something is written in the input the text in the span element is
update accordingly.
With chain
we can combine as many elements as we'd like. This
example combines a div
with a span
with a p
.
div().chain((_) => span("Text").chain((_) => p("More text")));
And the resulting HTML would look like this:
<div></div>
<span>Text</span>
<p>More text</p>
However, when we don't use the output from components we can instead
combine them with sequence_
.
sequence_(Component [div(), span("Text"), p("More text")]);
Components typically take child components as their last argument.
If instead of the above HTML we wanted this:
<div>
<span>Text</span>
<p>More text</p>
</div>
We could do
div(span("Test").chain((_) => p("More text")));
As a convenience we can also do
div([
span("Test"),
p("More text")
])
This is "sugar" for calling sequence_
on the array.
Often using chain
can be cumbersome since each chain
invocation
adds a layer of nesting. Instead we can use "go-notation".
component = go(function*() {
const {inputValue} = yield input();
yield p(inputValue);
});
Example
The example below creates an input field and print whether or not it
is valid.
import {map} from "jabz";
import {runMain, elements, loop} from "@funkia/funnel";
const {span, input, div} = elements;
const isValidEmail = (s: string) => s.match(/.+@.+\..+/i);
const main = go(function*() {
yield span("Please enter an email address: ");
const {inputValue: email} = yield input();
const isValid = map(isValidEmail, email);
yield div([
"The address is ", map((b) => b ? "valid" : "invalid", isValid)
]);
});
runMain("#mount", main);
A few explanations to the above code:
- The
go
function and the generator expresses do-notation, i.e.
monadic chaining. Here the monad is Component
. - The function
input
returns Component<{inputValue: Behavior<string>}>
. We yield
it which binds the inputValue
behavior to email
. - Next the
isValidEmail
predicate is mapped over the email
behavior and a div
component describing the validation status is
added.
Examples
Approximately listed in order of increasing complexity.
- Simple — Very simple example of an
email validator.
- Fahrenheit celsius — A
converter between fahrenheit and celsius.
- Zip codes — A zip code validator.
Shows one way of doing HTTP-requests with the IO-monad.
- Continuous time —
Shows how to utilize continuous time.
- Counters — A list of counters.
Demonstrates nested components, managing a list of components and
how child components can communicate with parent components.
- Todo — An implementation of the
classic TodoMVC application. Note: Routing is not implemented yet.
Getting started
Installation
npm install @funkia/funnel
Funnel uses two peer dependencies, that you'll need to install too:
npm install --save jabz hareactive
Documentation
Nothing here yet. See the examples.
Contributing
Run tests once with the below command. It will additionally generate
an HTML coverage report in ./coverage
.
npm test
Continuously run the tests with
npm run test-watch