README
FRPuccinno
:warning: This is still in alpha development. I would love to get more people testing this out and finding issues to be stamped out, but I wouldn't recommend using this on any big projects that require a reliable view layer just yet! :warning:
FRPuccino is a small UI library built on the foundation of Most.js
It's inspired heavily by Elm and Cycle.js
Here's a short "counter" example:
import { createElement, createApplication } from '@abradley2/frpuccino'
function update (model, addValue) {
return model + addValue
}
function view () {
return <div>
<button onclick={1}>Clicked {value} times!</button>
</div>
}
createApplication({
mount: document.getElementById('app'),
update,
view,
init: 0
}).run(0)
Installation
@most/core and a few other associated libraries are peer dependencies.
npm install --save @most/core @most/scheduler @most/types @abradley2/frpuccino
You will also likely find @most/dom-event and @most/adaptor very useful. But only add them as you find the need. I do recommend checking out the README of each so you can recognize when that need arises.
Core Concepts
You do not actually need to be familiar with FRP to make use of FRPuccino. Reactive Programming is more the underlying engine of the API than the API itself. It will be very helpful, however, if you are familiar with the concepts of Streams and Sinks.
API Usage and Tutorial
Only two methods are needed to create basic applications:
createElement
and createApplication
createElement
Function: createElement
is FRPuccino's "React.createElement" drop-in replacement.
This createElement is unique. It returns an un-mounted DOM element, with
a Stream
consisting of all bound event handlers.
Here's a short illustrative example
/** @jsx createElement */
import { createElement } from '@abradley2/frpuccino'
import { newDefaultScheduler } from '@most/scheduler'
const el = <div>
<button onclick='Hello there'>Say Hello</button>
<button onclick='Farewell for now!'>Say Goodbye</button>
</div>
document.body.appendChild(el)
const sink = {
event: (t, message) => { alert(message) },
end: () => {},
error: () => {}
}
el.eventStream.run(sink, newDefaultScheduler())
Our el
above is a simple Element
that we can append to our document.
But createElement
also casts all the registered event handlers on that
Element
and all it's children to a single eventStream
(or Stream<Action>
) at the top node.
We can run this stream similar to how we'd run any regular @most/core stream
Because all our event handlers are bound to strings the type of the event stream
is Stream<string>
.
createApplication
Function: createElement
is cool, but by itself we can't really create complicated
user interfaces. createApplication
is a way of "looping" the
eventStream
returned by createElement
into an update
function.
This cycle of update
-> event
-> createElement
-> update
forms the
basic flow of all applications.
createApplication({
// this is our initial update value!
init: 0,
// this is what our user interface looks
view: (currentState) =>(<div>
<button onclick={1}>Clicked {currentState} times</button>
</div>),
update: (currentState, value) => currentState + value,
// we need a place to attach our view to the document
mount: document.getElementById('application')
})
// to kick off the stream we need to emit an initial value.
// here we emit "0" because we won't want to increment our state
// until the user actually clicks the button
.run(0)
TaskCreator<Action>
Type: Our examples up until now have only dealt with event handlers bound
in our view. What if we want to execute and respond to an HTTP request?
What if we want to listen to an event that is scoped outside of our view
(such as window.onscroll
)? We can use
Tasks for this.
Tasks are helper functions that allow us to propagate events to Sinks
When we call createApplication
an
internal Sink
is created which does a couple things. It subscribes to the Stream
of
events returned by our view
and update
functions,
and the resulting DOM nodes created by composing update
with view
.
The definition is similar to
Sink<{eventStream: Stream<{action: Action}>, view?: Element}>
A "Task Creator" that creates a
Scheduled Task
to propagate events to this Sink
will
look something like this:
import { TaskCreator } from '@abradley2/frpuccino'
import { now, propagateEventTask } from '@most/core'
import { asap } from '@most/scheduler'
export function propagateEvent <Action> (action: Action): TaskCreator<Action> {
const event = { eventStream: now({ action }) }
return (sink, scheduler) => {
const task = propagateEventTask(event, sink)
return asap(task, scheduler)
}
}
UpdateResult<Model, Action>
Type: The main update
function is allowed to return more than just the next version
of the Model
. It may also return an array consiting of the model as the first
item, and either TaskCreator<Action>
or TaskCreator<Action>[]
as the
second item.
We can change our original update
function so when our application starts,
we use our propagateEvent
function to start us out with counter incrementing
once by a value of "1"
function update (currentState, value) {
// recall that we specified "0" as our initial action to dispatch when
// our application starts.
if (value === 0) {
return [currentState, propagateEvent(1)]
}
return currentState + value
}
UpdateResult<Model, Action>
is very flexible. We can not only give a single
scheduled task to be executed as a result of update, but many.
if (value === 0) {
return [
currentState,
[
propagateEvent(1),
propagateEvent(1)
]
]
}
mapElement
Method: Similar to how we can map
a Stream
from Stream<A> -> Stream<B>
we can use mapElement
to convert the eventStream
of one element
to another Stream
type. This is useful to avoid cases where
due to composing many different modular features we end up with
types like Stream<A | B | C | D | E>
that are difficult to reason
about in our application's update function.
Here's an illustrative example of avoiding an update function having to
deal with an event of type number | string
by normalizing
all eventStreams
to Stream<numer>
function button () {
return <div>
<button onclick={1}>Click me</button>
</div>
}
function input () {
return <div>
<input onchange={(e) => e.target.value} />
</div>
}
function application () {
return <div>
<div>
Count by one:
{button()}
</div>
<div>
Count by input:
{mapElement(
(payload) => {
if (!Number.isNaN(result)) return result
return 0
},
input()
)}
</div>
</div>
}
:warning: Due to performance reasons, mapElement
actually mutates
the Element
passed to it. Always use a constructor function to pass
the second argument to this function :warning: