Awesome
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.
<br> ## State/DataThe 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()
<br>
## 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', [])
}
<br>
## Updates
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.
<br> ## Folder ConventionI 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.
-
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">
<br>
## 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.
<br> ## Flavoursdist/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.