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.
## Components
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.
## State/Data
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()
## Defaults
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', [])
}
## Updates
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()
})
}
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.
## Events
Dispatch an event on the root element to communicate changes to parents (node.dispatchEvent
).
## Routing
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.
## Bundling
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.
## Folder Convention
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 require
able index.js
from a directory of resources.
## Debugging
-
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.
## Middleware
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:
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=..]' }
}
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 }}
}
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">
## Fullstack
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.
## Flavours
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.