Skip to content
/ un Public

unframework for universal uncomponents - use your uncomponents with no boundaries

Notifications You must be signed in to change notification settings

dmitriz/un

Repository files navigation

    __   __  _____   
   / /  / / / ___ \   ( Logo inspiration from MostJS
  / /  / / / /  / /     https://github.com/cujojs/most )
 / /__/ / / /  / /      
 \_____/ /_/  /_/

un.js

Unframework for Universal Uncomponents

We do not think in terms of reusable components. Instead, we focus on reusable functions. It is a functional language after all! -- Scaling The Elm Architecture

Contributions and Feedback are highly welcome!

Quick start

Install the un.js package with

$ yarn add un.js

or with

npm install un.js -S

or with pnpm

pnpm install un.js -S

and try the active counter example or read the introduction below.

Philosophy and Universality

  • Write your business logic as pure functions with no external dependencies
  • No side-effects, testable by your favorite test runners
  • No external imports, no packages, no libraries
  • No extension of proprietary component classes (Backbone, React, ...)
  • No lock-ins, your uncomponents should be usable as plugins with or without any framework
  • Reuse your code and share with others accross frameworks with no boundaries

Why unframework?

  • Frameworks try to provide and cater for everything, un tries the opposite - give you the maximal possible freedom.

  • Frameworks try to tell you exactly what to do, un tries the opposite - staying out of your way.

  • Frameworks make your code coupled with proprietary syntax, un lets you write your code with plain JavaScript functions, undistinguishable from any other functions. There is not a single trace of un in any of your functions.

  • Frameworks often like you to inherit from their proprietary classes, un tries to help you embrace the pure functional style and minimise use of classes and this. However, this is merely suggestive. Giving you maximum freedom and staying out of your way is a higher priority for un.

What is provided?

Currently a single tiny factory function called createMount. See here the complete code. Its role is similar to React.render, in which you would typically see it in only few places in your app.

Here is a usage example. Instead of learning new API, new framework or long set of new methods, your simply import your favorite familiar libraries that you are already using anyway:

const mount = createMount({ 

  // your favorite stream factory
  // mithril/stream, TODO: flyd, most, xstream
  createStream: require("mithril/stream"),

  // your favorite element creator
  // mitrhil, TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript
  createElement: require('mithril'),

  // your favorite create tags helpers (optional)
  createTags: require('hyperscript-helpers'),

  // mithril.render, TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith
  createRender: element => vnode => require('mithril').render(element, vnode)
})

So instead of having external dependencies in every file, un simply lets you provide those libraries once and return the mount function, the only function from un that you need. The role of the mount is similar (and inspired by) Mithril m.mount or React.render with auto-redrawing facility. Our key vision is, attaching a live component to an element should be as simple as calling a function and mount does exactly that:

// mount our live uncomponent and get back its writeable stream of actions
const actions = mount({ element, reducer, view, initState})

So we call mount with 4 basic properties:

  • element: HTML element, where we attaching our uncomponent, similar to React.render the element's content will be overwritten

  • reducer: Redux style reducer from our model logic

  • view: Plain pure function taking dispatcher and state and returning new state, the state can be global, narrowed down, or completely local to the uncomponent, to cater for the fractal architecture. The view function dispatches actions just like in Redux and returns a virtual or real DOM element, depending on the library used in configuring the mount. But to be completely pure with no external dependency, the view must include the element creator factory as one of its parameters:

// all parameters are explicit, no dependencies, no magic
// can be tested as pure dumb function in any environment
const view = h => (state, dispatch) => 
  h('div', `Hello World, your ${state} is wonderful!`)

where h stands for our favorite element creator passed to createMount. We find the hyperscript API supported by many libraries (e.g. Mithril, Snabbdom, or react-hyperscript) most convenient, but using JSX should also be possible as it is equivalent to the React.createElement calls.

Or use the createTags helpers (like hyperscript-helpers) that you can conveniently destructure inside the view:

// again, no dependencies, only function parameters, 
// all inputs are instantly visible, no need to jump elsewhere
const view = ({ div }) => (state, dispatch) => 
  div(`Hello World, your ${state} is wonderful!`)

The other two parameters of the view are dispatch and state that match the types (or more precisely, interfaces) of the action and the state parameters of the reducer. (Note how the view signature matches the one of the reducer. Further, it also matches the native JS Array.prototype.reduce as well as the general reduce method signatures, the latter provided by the Foldable Typeclass.) The state, updated by the reducer, will be passed directly to the view (from the same mount call). And every action value used to call the dispatch function, will be passed directly as action to the reducer. For example, calling dispatch('foo') in the event handler inside the view will result in foo being passed as action to the reducer.

This style of writing was inspired by https://github.com/ericelliott/react-pure-component-starter and https://medium.com/javascript-scene/baby-s-first-reaction-2103348eccdd

In React the role of the view would be played by the component render method, but we already have another static method React.render, so we prefer to call it the view as in Mithril.

  • initState: The state to initialise our uncomponent.

Uncomponents

Why "uncomponent"? Because there isn't really much of a "component", the reducer and the view are just two plain functions and the initial state is a plain value.

So what is called "uncomponent"?

  • Native JavaScript functions. Or native generators. Or native object holding a few functions.
  • No proprietary syntax. Every part of the framework, library, package is hidden away from the user. In the configuration.
  • Pluggable into any framework. Or into no framework. This is what configuration is for.

Streams

Streams are in the core of un. The introduction to Reactive Programming you've been missing by Andre Staltz is a great introduction to streams. flyd is a great minimal but powerful stream library to use, including great examples to see the streams in action. The Mithril stream library is even smaller but suffices to let un do its job. Note that some libraries, such as most distinguish between "pending" and "active" streams, but to make things as simple as possible, all streams in un are always active, readable and writeable.

Despite of their initial complexity, streams model very well the asynchronous processes such as user acton flow, and consequently help to greatly simplify the architecture. The state values are stored directly inside the stream, so no stores such as in Redux are needed.

Instead of letting the framework do some "magic" behind the scene, when updating the DOM, with un, your view listens to its state stream. Whenever the state changes, its new value is passed to any subscriber, or which the view is one. The view function is pure with no side-effects, so all it does is pass the new updated element to the rendering library you provided to to createMount. It is then the library's responsibility to create the side-effect updating the DOM.

Note that you can absolutely ignore the stream part and write your code without seeing a single stream. Like in Redux, every action passed to dispatcher will go through the cycle. However, using streams can give you additional control. The mount method returns the action stream that is both readable and writeable. That means, you can attach other subscribers to your actions, or you can actively pipe new values into it, causing the same or additional actions passed to the reducer and subsequently updating the DOM.

A basic example below is demonstrating how the action stream can be externally driven in addition to user actions.

Full reactive control of your uncomponents

The un mount function, created as described above, returns for every uncomponent, the object

const { 
  states: streamOfStates, 
  actions: streamOfActions 
} = mount(...)

holding the state and action streams (we like to refer to streams by plurals to emphasize their collection nature).

That means, you can conveniently add any complex behavior (such as loading external data) to your uncomponent by piping the external actions into its action stream, or you can attach an external subscriber to the state stream, to be updated on any state changes in a reactive fashion.

The active-counter example demonstrates this feature, see below.

un reactive vision

Right now the streams provided by un conform to the Mithril Stream API

In order to make using un as universal and painless as possible, and accessible to broader audience, we would like to facilitate plugging other stream libraries. So you can use your favorite stream api to control your uncomponents.

Help and contributions are welcome!

Pure reducer function

Our both state and action values are just numbers and the reducer simply adds the action value to the state:

const reducer = (state, action) => 
  state + action

By making the state local and avoiding giving specific names to the actions, we can make the reducer function more of a general purpose and reusable. Just like a function in Ramda or similar library.

Pure view function

Our view function example here demonstrates how a function helper inside can reuse all the arguments of the outside function:

const view = ({ button }) => (state, dispatch) => {

  // reusing the button function
  const change = amount => 
    button( 
      {onclick: () => dispatch(amount)}, 
      (amount > 0) 
        ? `+${amount}` 
        : `-${-amount}`
      )

  return [  
    `Increasing by 5 every second: `,
    change(10),
    ` ${state} `,
    change(-10)
  ]
}

Here we attach to the onclick listener (following Mithrils API flavour) the anonymous function passing the amount to the dispatch. As mentioned above, that value is passed as action to the reducer.

Behind the scene, every time the user clicks that button, the amount value is written into the action stream. That is essentially what the dispatch function does.

Use with HTTP responses, Promises and other external actions

In addition to user's actions, the counter is being updated from the application via this code:

const delayedConstant = (val, delay) => stream => {
  setInterval(() => stream(val), delay)
  return stream
}
delayedConstant(5, 1000)(actions)

The actions stream here is exported from the un mount function and gives access to the external drivers. The simple periodic values here are for demonstration purposes. They can be replaced by any HTTP request or any value returned by a JS Promise, or can be even subscribed to another stream, such as event stream from any event:

// the response value from the promise will appear in the actions stream
// once the promise is resolved or the error object if the promise is rejected
actions(fetch(someUrl))

// every value from the `externalStream` 
// will be passed down the actions stream in real time
externalStream.map(actions)

The functionality here is based on the getter-setter syntax of the flyd stream library and the way promises are treated. However, any another stream library with stream updates functionality (e.g. mostjs-subject) can be used instead.

Once subscribed as above, all values will be automatically passed to the reducer as action values. That way all business logic needed can be put as pure function updating the state inside the reducer.

The rest is full automatic: reducer runs on every new action, the state is updated and passed to the view that will be sent to the renderer to update the dom.

The mount function

// the only method you ever use from 'un'
const createMount = require('un.js')

// or React.createElement

const mount = createMount({ 

  // your favorite stream factory
  // TODO: flyd, most, xstream
  createStream: require("mithril/stream"),

  // your favorite element creator
  // TODO: (React|Preact|Inferno).createElement, snabbdom/h, hyperscript
  createElement: require('mithril'),

  // your favorite create tags helpers
  createTags: require('hyperscript-helpers'),

  // TODO: (React|Preact|Inferno).render, snabbdom-patch, replaceWith
  createRender: element => vnode => require('mithril').render(element, vnode)
})

// create dom element
const e = document.createElement('div')
document.body.appendChild(e)

// mount our live uncomponent and get back its writeable stream of actions
const actions = mount({ e, reducer, view, initState: 0 })

More Examples

The Basic Examples are intentionally made very simple and focused.

The submit example demonstrates how to attach an simple update action to the onsubmit event of the <form>.

The submit-with-reset example in addition resets the input field after submission by providing additional state variable to control it.

The keypress example demonstrates how to pass all keys pressed to the dispatcher, and then via the reducer, back to the state.

The todos example demonstrates a basic interactive todos collector.

The todos-with-delete example adds the delete feature to the previous example. It deviates slightly from the traditional todos examples, in which the deletion is done by name, i.e. all todos with the same name are deleted at the same time.

More advanced examples are in active development, help and contributions are welcome!

Inspirations (incomplete and in random order)

Professor Frisby's Mostly Adequate Guide to Functional Programming by the same author.

Professor Frisby Introduces Composable Functional JavaScript

The introduction to Reactive Programming you've been missing by Andre Staltz, as well as many other of his always enlightening posts as well as his CycleJS project

@sindresorhus on reusable modules

The Ramda and its sister Fantasy Land and Sanctuary projects

The Mithril framework

The simple but powerful flyd stream library

The new Functional Reactive Turbine framework and the oder Functional Frontend Architecture by @paldepind

The Snabbdom virtual dom library and the broader Hyperscript Ecosystem

The Redux and Elm projects

TodoMVC example built with most.js

Meiosis

redux-react-local

...To be extended...

About

unframework for universal uncomponents - use your uncomponents with no boundaries

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published