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

hyperapp-map

v2.0.0

Published

utility for advanced state management in hyperapp

Downloads

17

Readme

Hyperapp Map

This is a utility for Hyperapp (v2.0.3 and above). It lets you structure your app by composing self-contained, reusable modules with minimal boilerplate, while staying true to the single-state, one-way-data-flow paradigm.

Installation

Minified IIFE from CDN in script tag

Add this script tag to your HTML

<script src="https://unpkg.com/hyperapp-map/dist/hyperappmap.js"></script>

It will export hyperappmap in your global scope. It is an object containing the exported functions:

const { makeMap, mapVNode, mapPass, mapSubs } = hyperappmap

Import as ES module from CDN

Import directly to your scripts from CDB:

import {
    makeMap,
    mapVNode,
    mapPass,
    mapSubs,
} from 'https://unpkg.com/hyperapp-map'

Install into bundled project from npm

If you're bundling your app, you can install the package via

npm i hyperapp-map

And then import in your scripts like this:

import { makeMap, mapVNode, mapPass, mapSubs } from 'hyperapp-map'

Introduction by Example

Let's say you defined a self contained counter module:

const init = 0

export const Increment = state => state + 1
export const Decrement = state => state - 1

export const view = state =>
    h('p', {}, [
        h('button', { onclick: Decrement }, '-'),
        state,
        h('button', { onclick: Increment }, '+'),
    ])

Now you want to integrate it as a part of a larger app with other moving parts:

app.js

import * as foo from './foo.js'
import * as counter from './counter.js'

app({
    init: {
        foo: foo.init,
        counter: counter.init,
    },
    view: state =>
        h('main', {}, [foo.view(state.foo), counter.view(state.counter)]),
    node: document.getElementById('app'),
})

Unfortunately, that won't work, because the state given to the counter actions (Increment, for example) will be the app's full state {foo: ..., counter: 0} - not simply the expected 0.

The counter actions would need to be defined as:

const Increment = state => ({ ...state, counter: state.counter + 1 })
const Decrement = state => ({ ...state, counter: state.counter - 1 })

but if we changed the implementation of counter.js to fit this particular app, it would no longer be truly self contained and reusable. Imagine, for example if we wanted a second counter in the app –– how would we define the actions then?

While the actions in counter.js should be strictly limited to defining operations on counter state, app.js has the knowledge of how to extract current counter state from current full state and how to merge next counter state to produce the next full state. Combine this knowledge with the exported Increment to define an action that works in this particular app:

const CounterIncrement = state => ({
    ...state,
    counter: counter.Increment(state.counter),
})

Since we want to do the same for Decrement we abstract out the action-map:

const counterMap = action => state => ({
    ...state,
    counter: action(state.counter),
})
const CounterIncrement = counterMap(counter.Increment)
const CounterDecrement = counterMap(counter.Decrement)

Here counterMap is an "action-map", which is to say a function that defines an action from another action. This particular action-map restricts the scope of the given action to the counter prop of the full state.

Defining action-maps is the first thing hyperapp-map can help you with, through the makeActionMap function. Our counterMap could as well have been defined like this:

const counterMap = makeActionMap(
    //how to get counter state from full state:
    state => state.counter,

    // how to merge the resulting counter state with the full state:
    (state, counter) => ({ ...state, counter })
)

In this case there is no real benefit, since the first counterMap definition isn't much more complex. But what one of the actions we want to map returns an effect? What if Increment were defined as:

//Increments the value, and two seconds later decrements it again:
const Increment = state => [state + 1, delay(Decrement, 2000)]

Since app.js shouldn't know any implementation details about counter.js, we can't technically assume there won't bee effects returned. Writing an action-mapper that handles effect return values, mapping the actions dispatched from those effects in the same way, is nontrivial. But makeActionMap we can define such actions.

But making action-maps is just half the problem. Getting mapped actions in to the view, is the other half of the problem. And this is where mapVNode comes in. It takes an acition-map and a virtual node, scans through the entirety of the virtual node and replaces any actions, with actions mapped with the given map.

Now app.js can look like this:

import {makeMap, mapVNode} from 'hyperapp-map'
import * as counter from './counter.js'
import * as foo from './foo.js'

const counterMap = makeMap(x => x.counter, (x, counter) => ({...x, counter}))
const fooMap = makeMap(x => x.foo, (x, foo) => ({...x, foo}))

app({
    init: {counter: counter.init, foo: foo.init},
    view: state => h('main', {}, [
        mapVNode(counterMap, counter.view(state.counter))
        mapVNode(fooMap, foo.view(state.foo))
    ])
})

Using this method you may eventually find yourself at an impasse. What if you have some stateful component which is a container of somethig, like a modal dialog window that can be dragged around a window but can contain views from other actions. What if you have:

...
view: state => h('main', {}, [
    mapVNode(
        modalMap,
        modal.view(state.modal, {title: "Play with the counter"}, [
            mapVNode(
                counterMap,
                counter.view(state.counter)
            )
        ])
    )
])
...

With this structure, all the actions in counter.view will be transformed as:

action => modalMap(counterMap(action)

...which will make them not work.

This is the reason mapPass exists. It takes an array of vnodes (or a single one) and grants each action therein a one-time shield of protection from mapping. It means they will be exempt from mapping by the next map out (only that one).

Hence, this will work as exepcted:

view: state =>
    h('main', {}, [
        mapVNode(
            modalMap,
            modal.view(
                state.modal,
                { title: 'Play with the counter' },
                mapPass([mapVNode(counterMap, counter.view(state.counter))])
            )
        ),
    ])

The action map in the counter.view is now:

action => modalMap(mapPass(counterMap(action)))

And since mapPass cancels out modalMap in this case, all that is left is counterMap, which is what we wanted.

Finally, what if a module exports subscriptions? Then there is mapSubs, which takes a a map and an array of subscriptions. Each one will have its actions mapped by the given map. There is no equivalent to mapPassfor subscriptions because it isn't necessary.