0.1.1 • Published 7 years ago

rijs.minimal v0.1.1

Weekly downloads
2
License
pemrouz.mit-licen...
Repository
github
Last release
7 years ago

Minimal

Tiny (~4kB), super-fast, refined, reactive, fractal, unidirectional, isomorphic, pro-standards, declarative, Just Works™ framework for frontend development. There's only one API:

import ripple from 'rijs.minimal'

ripple(name)       // getter
ripple(name, body) // setter
ripple.on('change', (name, change) => {})

Get and set things in single store. A change event is emitted when something is updated, enabling reactive updates.

Define a component:

ripple('my-component', () => {})

Use it on the page:

<my-component>

Ripple is agnostic to how you write your components, they should just be idempotent (a single render function). This is fine:

ripple('my-app', (node, data) => node.innerHTML = 'Hello World')

Or using some DOM-diff helper:

ripple('my-app', (node, data) => jsx(node)`<h1>Hello World</h1>`)

Or using once/D3 joins:

ripple('my-app', (node, data) => {

  once(node)
    ('h1', 1)
      .text('Hello World')

})

For more info about writing idempotent components, see this spec.

The first parameter of the component is the node to update.

The second parameter contains all the state and data the component needs to render:

export default function component(node, data){ ... }

You can pass down data by adding the name of the resources to the data attribute:

<my-shop data="stock">
export default function shop({ stock }){ ... }

Declaring the data needed on a component is used to reactively rerender it when the data changes.

The other option is to explicitly pass down data to the component using the (D3) data binding:

once(node)
  ('my-shop', { stock })

If you want to just use DOM, you can invoke .draw() on a custom element to redraw it:

const shop = document.createElement('my-shop')
document.body.appendChild(shop)
shop.state = { stock }
shop.draw()

You can set defaults using the ES6 syntax:

export default function shop({ stock = [] }){ ... }

If you need to persist defaults on the component's state object, you can use a small helper function:

export default function shop(state){ 
  const stock = defaults(state, 'stock', [])
}

Local state

Whenever you need to update local state, just change the state and invoke a redraw (like a game loop):

export default function shop(state, i, el){ 
  const o = once(el)
      , { counter = 0 } = state

  o('span', 1).text(counter)
  o('button', 1)
    .text('increment')
    .on('click' d => {
      state.counter++
      o.draw()
    })
}

Global state

Whenever you need to update global state, you can simply compute the new value and register it again which will trigger an update:

ripple('stock', {
  apples: 10
, oranges: 20
, pomegranates: 30
})

Or if you just want to change a part of the resource, use a functional operator to apply a finer-grained diff and trigger an update:

update('pomegranates', 20)(ripple('stock'))
// same as: set({ type: 'update', key: 'pomegranate', value: 20 })(ripple('stock'))

Using logs of atomic diffs combines the benefits of immutability with a saner way to synchronise state across a distributed environment.

You can also use the list of all relevant changes since the last render in your component via element.changes to make it more performant.

Dispatch an event on the root element to communicate changes to parents (node.dispatchEvent).

Just invoke a redraw of your application when the route has changed:

export function app(node, data){
  const o = once(node)

  o('h1', 1)
    .text('You are currently on: ' + location.pathname)

  window.on('change', d => node.draw())
}

Decouter emitterifies window to give you the change event, go(url) for navigating, and sets location.params with current route parameters.

Ripple does not care how you load/bundle your resources. You only just need to register them at some point. This means you are free to use whatever tool chain you like:

// index.js
ripple('my-app', require('./resources/my-app'))
ripple('my-app.css', file('./resources/my-app.css'))
ripple('somedata', require('./resources/data/some'))
$ browserify index.js > app.js
<script src="app.js"></script>
<my-app></my-app>

An application is just a component that composes other components, so you shouldn't need any other scripts.

I recommend using the folder convention: a resources directory, with a folder for each component, and a data folder for data resources.

resources
├── data
│   ├── stock.js
│   └── order.js
└── my-app
│   ├── my-app.js
│   ├── my-app.css
│   └── test.js
└── another-component
    ├── another-component.js
    ├── another-component.css
    └── test.js

You can then use a helper script to automatically generate a single requireable index.js from a directory of resources.

  • Check ripple.resources for a snapshot of your application. Resources are in the tuple format { name, body, headers }.

  • Check $0.state on an element to see the state object it was last rendered with or manipulate it.

By default the draw function just invokes the function on an element. You can extend this without any framework hooks using the explicit decorator pattern:

// in component
export default function component(node, data){
  middleware(d, i, el)
}

// around component
export default middleware(function component(node, data){
  
})

// for all components
ripple.draw = middleware(ripple.draw)

A couple of useful middleware included in this build are:

Needs

This middleware reads the needs header and applies the attributes onto the element. The component does not render until all dependencies are available. This is useful when a component needs to define its own dependencies.

export default {
  name: 'my-component'
, body: function(){}
, headers: { needs: '[css=..][data=..]' }
}

Helpers

This middleware makes the specified helper functions available from the resource (hidden properties). This is useful to co-locate all logic for each resource in one place.

export default {
  name: 'stock'
, body: {}
, headers: { helpers: { addNewStock, removeStock }}
} 

Styling

Stylesheet(s) can be modularly applied to an element: This middlware simply reads the css attribute and inserts them in the shadow root or scopes them and adds to head:

ripple('some.css', `:host { background: red }`)
<head>
  <style>my-shop { background: red }</style>
</head>
<my-shop css="some.css"> 

If you have a backend for your frontend, checkout rijs/fullstack which transparently adds a few more modules to synchronise state between client-server or for more docs.

You can also adjust your own framework by adding/removing modules.

dist/ripple.js provides ripple and also some small, useful, high power-to-weight ratio functions that enriches the language grammar. If you don't want the helper functions, use dist/ripple.pure.js. Add .min for prod. Minified and gzipped the sizes are ~12kB and ~4kB respectively.