npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@abradley2/frpuccino

v1.0.0-alpha.3

Published

A lightweight reactive UI library built on @most/core

Downloads

7

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

Function: createElement

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>.

Function: createApplication

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)

Type: TaskCreator<Action>

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)
  }
}

Type: UpdateResult<Model, Action>

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)
    ]
  ]
}

Method: mapElement

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: