redda

A functional approach to a JSONML based UI

Usage no npm install needed!

<script type="module">
  import redda from 'https://cdn.skypack.dev/redda';
</script>

README

Redda

A functional approach to a JSONML based UI

You can check out the LIVE demo.

Installation

You can just use the npm package.

npm i redda

Or download this repo and include build/index.js in your app.

Description

The goal of the project is to create a UI framework that reads data and produces HTML. The basis is to use JSONML and augment it with the ability to use functions or symbols as tag-name.

If you're using the npm package, go on and require Redda.

const redda = require('redda')

// or import if using babel or something

import redda from 'redda'

Elements

Elements are the basic building blocks that Redda uses to display content. All JSONML are valid elements. On top of that it can be a function returning a JSONML. One notable mention is that a Redda element can use a Symbol as tag name for example Symbol.for('div'). All native HTML5 elements are mapped to symbols accessable from redda.dom by destructuring.

The basic JSONML

const basic_element = ['div', { class: 'fancy' }, "Content text"]

// ...
<div class="fancy">Content text</div>
const { div } = redda.dom
const sym_element = [div, { class: 'fancy' }, "Content text"]

// ...
<div class="fancy">Content text</div>

Function elements

Elements as mentioned above can be functions to break out of staticity. Functional elements should be thought like "tags". The most basic usage would be like this.

const { div } = redda.dom
const fn_element = () => [div, { class: 'fancy' }, "Content text"]
const app = [fn_element]

// ...
<div class="fancy">Content text</div>

That does note display all the possibilities using functions. When we say you need to think about them as "tags", we mean it can be used as tags, in which case it will receive it's "context" as arguments. Please consider the following example.

const { div } = redda.dom
const fn_element = (attrs, ...cont) => [div, { ...attrs, class: 'fancy' }, ...cont]
const app = [fn_element, { id: 'your_elem' }, "Content provided outside"]

// ...
<div id="your_elem" class="fancy">Content provided outside</div>

As seen the function used as a tag receives arguments. The first one is an Object, and the rest of the encolsing Array is considered to be the content, and is spreaded for our function. If the item following our function is not an object, an empty object will be passed instead to keep signature consistent.

const { div } = redda.dom
const fn_element = (attrs, ...cont) => [div, { ...attrs, class: 'fancy' }, ...cont]
const app = [fn_element, "Content provided outside", "and some more"]

// ...
<div class="fancy">Content provided outside and some more</div>

Rendering

To render our app we use the method render provided by Redda. This needs an element to render your app into, and the app itself as follows. The former is a native DOM Node, the latter is a JSONML array.

const { h1 } = redda.dom
const render = redda.render(document.getElementById('app-cont'), [h1, 'Hello World!'])

Render will automatically render your app into the desired container node, and also return a function to trigger rerender. It will render like this.

<div id="app-cont">
  <h1>Hello World!</h1>
</div>

State

All applications has a state. To solve this, Redda uses a Redux like state store with actions. There are some differences tho.

Caution! Redda's state uses function names to identify state fragments and actions to dispatch. You'll need to export and import your fragments and actions once defined.

The state store

To create a state store just instantitate one.

const state = redda.state()

This will handle state management through fragments registered and actions dispatched which will be familiar if you've used Redux before. The state in the previous example will return an object providing the state management methods you'll need. One of these is get, which will return current state. It's still empty tho.

state.get() // => {}

Adding a fragment

A fragment is very simple. It's a named function, returning the initial value for the fragment. The name will be used to register the fregment by. To make this clear please see the exampel below, keeping in mind we created a state in the previous. We'll use the method add.

const fragment = () => ({ value: 0 })

state.add(fragment)

state.get() // => { "fragment": { "value": 0 } }

We now officially have more then an empty state.

Actions and dispatching them

Actions are also simple named functions. They differ from fragments in that they receive the current state to operate on and return a new state. Similar to how you would do with Redux. Let's follow the example we started with some adjustments.

const set_value = ({ value, ...state }, new_value) => ({ ...state, value: new_value })

We need to register this along the fragment we created to make it use it. We can reuse this action with any other fragment we create. To dispatch it use the method provided by state as disp.

state.add(fragment, set_value)

state.disp(set_value, 1)

state.get() // => { "fragment": { "value": 1 } }

Connecting elements with state

This is where we make use of our state. We take a simple element as described above and connect it to our state. Still following the example we used.

Note that the signature of the element with state changes, as the first argument is the state, followed by the attributes and content.

const { h1 } = redda.dom
const element = ({ fragment: { value } }, attrs, ...cont) =>
  [h1, { ...attrs }, `Value is ${value}`, ...cont]

To connect the element use the state provided method conn. First we need to specify the element we want to connect and then we provide the fragments we want to connect. We use the reference for these.

const element_with_fragment = state.conn(element, fragment)
state.disp(set_value, 2)

Now, whenever we call our new method returned by conn, it'll be provided with the current state. This is an object with keys by the name of the fragments, holding the actual values of those in the state store. Rendering our element we'll see it.

<h1>Value is 2</h1>

Rerender app on state change

To reflect state changes we need to use the state's on_change method and pass the render method provided by redda.render(...).

const render_app = redda.render(document.getElementById('app-cont'), [element_with_fragment])

state.on_change(render_app)

Now, after state.disp(set_value, 3) our app will rerender and look like this:

<h1>Value is 3</h1>

Event handling

All modern apps need event handling. While you could render the site, and attach event listeners yourself, it is included in Redda. For this it utilizes the standard HTML event listener attributes like onclick. mousedown, etc. You just need to include these in your attrs object.

Note that all attributes starting matching the regexp /^on/ will be treated as en event handler.

const { button } = redda.dom
const element = () => [button, { onclick: (ev) => console.log(ev) }, 'CLICK ME']

redda.render(document.getElementById('app-cont'), [element])

This will result in the following HTML:

<button onclick-key="06226f8930feec">CLICK ME</button>

Redda handles event listeners in a single store. This is to achieve listener detaching before each render, to ensure the reuse of an element will not result in multiple or unwanted listeners attached.

Second render

First Redda will use innerHTML of the node to inject the stringified HTML created by the JSONML you passed. On the second render though, it will try to reuse the existing DOM structure. While it could be still optimized further, this will allow a as seemless of an update as possible.

On the second render event handlers wont't be registered and assigned in an attribute, but will be assigned through the element's property, like node.onclick = () => null for example.

Examples

To see an example please clone repo and browse the examples folder.

$ https://github.com/rangeoshun/redda.git
$ cd redda

Clock example

A simple state demo, displaying current time.

$ open examples/clock/index.html

Counter demo

A more advanced demo, demonstrating the event handling through a counter which you can increment or decrement.

$ open examples/counter/index.html

TODOs demo

An even more advanced demo displaying possibilities with Redda. It implements a todo app using materialize.

You can check out the LIVE demo.

$ open examples/todo/index.html

Screenshot of TODOs demo

To come

  • State management
  • Event handling
  • Reuse existing DOM
  • Meaningful error messages
  • Add more tests
  • Other goodies

Instructions

Initial steps

Prerequisites

You'll need Node.js (at least v8.11 is recommended) and yarn for dependency management, and rollup installed globally.

yarn global add rollup

The plan is to have a docker environment for development, but you'll need dependencies installed locally for now.

Dependencies

After checking out the repo, execute following command in project root. This will install developement dependencies. One of the goals of this project is not to have any outter dependencies for final build.

yarn

Running tests

You can use the yarn script executing the following command. This will run jest tests in watch mode.

yarn test

To check out coverage, use this handy script.

yarn coverage

If you need more fancy stuff, you after running the above command, open coverage report in your browser.

open ./coverage/lcov-report/index.html

Building

Use rollup to bundle package via:

yarn build