rijs
Version:
On the server:
385 lines (262 loc) • 11.3 kB
Markdown
# Ripple Fullstack
On the server:
**`index.js`**
```js
const ripple = require('rijs')({ dir: __dirname })
```
On the client:
**`pages/index.html`**
```html
<script src="/ripple.js"></script>
```
Run it:
```
$ node index.js
```
This starts up a server on a random port and statically serves your `/pages` directory. You can also specify a `port` to always use, or pass an existing HTTP `server` (e.g. from express).
Clients will then just be streamed the fine-grained resources they are using (i.e. everything is lazy loaded, no bundling, no over-fetching).
Ripple keeps clients/servers in sync by replicating an immutable log of actions in the background, and subsequently the view - or other modules - which are reactively updated when the local store is updated.
That's it! No boilerplate necessary, no build pipeline, no special transpilation, no magical CLI.
The basic API is:
```js
ripple(name) // getter
ripple(name, body) // setter
ripple.on('change', (name, change) => { .. })
```
## Components
Let's add a (Web) Component to the page:
**`index.html`**
```diff
<script src="/ripple.js"></script>
+ <my-app></my-app>
```
Let's define the component:
**`resources/my-app.js:`**
```js
export default () => ()
```
Ripple is agnostic to _how_ you write your components, they should just be idempotent: a single render function.
This is fine:
**`resources/my-app.js:`**
```js
export default (node, data) => node.innerHTML = 'Hello World!'
```
Or using some DOM-diff helper:
**`resources/my-app.js:`**
```js
export default (node, data) => jsx(node)`<h1>Hello World</h1>`
```
Or using [once](https://github.com/utilise/once#once)/D3 joins:
**`resources/my-app.js:`**
```js
export default (node, data) => {
once(node)
('h1', 1)
.text('Hello World')
})
```
For more info about writing idempotent components, see [this spec](https://github.com/pemrouz/vanilla).
## 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:
```js
export default function component(node, data){ ... }
```
* You can inject data resources by adding the name of the resources to the data attribute:
```html
<my-shop data="stock">
```
```js
export default function shop({ stock }){ ... }
```
Declaring the data needed on a component is used to reactively rerender it when the data changes.
Alternatively, you can use `ripple.pull` directly to retrieve a resource, which has similar semantics to [dynamic `import()`](https://github.com/tc39/proposal-dynamic-import) (i.e. resolves from local cache or returns a single promise):
```js
const dependency = await pull('dependency')
```
* The other option is to explicitly pass down data to the component from the parent:
```js
once(node)
('my-shop', { stock })
```
The helper function will set the state and redraw, so redrawing a parent will redraw it's children. If you want to do it yourself:
```js
element.state = { stock }
element.draw()
```
## Defaults
You can set defaults using the ES6 syntax:
```js
export default function shop({ stock = [] }){ ... }
```
If you need to persist defaults on the component's state object, you can use a small [helper function](https://github.com/utilise/utilise#--defaults):
```js
export default function shop(state){
const stock = defaults(state, 'stock', [])
}
```
## Updates
#### Local state
Whenever you need to update local state, just change the `state` and invoke a redraw (like a game loop):
```js
export default function abacus(node, state){
const o = once(node)
, { counter = 0 } = state
o('span', 1)
.text(counter)
o('button', 1)
.text('increment')
.on('click.increment' 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:
```js
ripple('stock', {
apples: 10
, oranges: 20
, pomegranates: 30
})
```
Or if you just want to change a part of the resource, use a [functional operator](https://github.com/utilise/utilise#--set) to apply a finer-grained diff and trigger an update:
```js
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.
Components are rAF batched by default. You can access the list of all relevant changes since the last render in your component via `node.changes` to make it more performant if necessary.
## Events
Dispatch an event on the root element to communicate changes to parents (`node.dispatchEvent`).
## Routing
Routing is handled by your top-level component: Simply parse the URL to determine what children to render and invoke a redraw of your application when the route has changed:
```js
export function app(node, data){
const o = once(node)
, { pathname } = location
o('page-dashboard', pathname == '/dashboard')
o('page-login', pathname == '/login')
once(window)
.on('popstate.nav', d => o.draw())
}
```
This solution is not tied to any library, and you may not need one at all.
For advanced uses cases, checkout [decouter](https://github.com/pemrouz/decouter).
## Styling
You can author your stylesheets assuming they are completely isolated, using the Web Component syntax (`:host` etc).
They will either be inserted in the shadow root of the element, or scoped and added to the head if there is no shadow.
By default, the CSS resource `component-name.css` will be automatically applied to the component `component-name`.
But you can apply multiple stylesheets to a component too: just extend the `css` attribute.
## Folder Convention
All files in your `/resources` folder will be automatically registered (except tests etc). You can organise it as you like, but I recommend using the convention: a folder for each component (to co-locate JS, CSS and tests), and a `data` folder for the resources that make up your domain model.
```
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
└── ...
```
Hot reloading works out of the box. Any changes to these files will be instantly reflected everywhere.
## Loading Resources
You can also get/set resources yourselves imperatively:
```js
ripple(name) // getter
ripple(name, body) // setter
```
Or for example import resources from other packages:
```js
ripple
.resource(require('external-module-1'))
.resource(require('external-module-2'))
.resource(require('external-module-3'))
```
You can also create resources that proxy to [fero](https://github.com/pemrouz/fero)) services too.
## Offline
Resources are currently cached in `localStorage`.
This means even _before_ any network interaction, your application renders the last-known-good-state for a superfast startup.
Then as resources are streamed in, the relevant parts of the application are updated.
Note: Caching of resources will be improved by using ServiceWorkers under the hood instead soon ([#27](https://github.com/rijs/fullstack/issues/27))
## Render 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:
```js
// in component
export default function component(node, data){
middleware(node, data)
}
// around component
export default middleware(function component(node, data){
})
// for all components
ripple.draw = middleware(ripple.draw)
```
A few useful middleware included in this build are:
### Needs
[This middleware](https://github.com/rijs/needs#ripple--needs) 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. You can also supply a function to dynamically calculate the required resources.
```js
export default {
name: 'my-component'
, body: function(){}
, headers: { needs: '[css=..][data=..]' }
}
```
### Shadow
If supported by the browser, a shadow root will be created for each component. The component will render into the shadow DOM rather than the light DOM.
### Perf (Optional)
This one is not included by default, but you can use this to log out the time each component takes to render.
Other debugging tips:
* Check `ripple.resources` for a snapshot of your application. Resources are in the [tuple format](https://github.com/rijs/core#ripple--core) `{ name, body, headers }`.
* Check `$0.state` on an element to see the state object it was last rendered with or manipulate it.
## Sync
You can define a `from` function in the resource headers which will process requests from the client:
```js
const from = (req, res) =>
req.data.type == 'REGISTER' ? register(req, res)
: req.data.type == 'FORGOT' ? forgot(req, res)
: req.data.type == 'LOGOUT' ? logout(req, res)
: req.data.type == 'RESET' ? reset(req, res)
: req.data.type == 'LOGIN' ? login(req, res)
: false
module.exports = {
name: 'users'
, body: {}
, headers: { from }
}
```
This can return a single value, a promise or a stream. On the client you make requests with `ripple.send(name, type, value)`. This returns an awaitable [stream](https://github.com/utilise/emitterify/#emitterify).
You can also use the `.subscribe` API to subscribe to all or part of a resource. The key can be arbitrarily deep, and multiple keys will be merged into a single object.
```js
ripple.subscribe(name, key)
ripple.subscribe(name, [keys])
```
Subscriptions are automatically deduplicated are ref-counted, so components can indepedently subscribe to the data they need without worrying about this.
Note that you can also use `ripple.get` instead of subscribe if you just want to get a single value and then automatically unsubscribe.
## Ripple Minimal
If you have don't have backend for your frontend, checkout [rijs/minimal](https://github.com/rijs/minimal) which is a client-side only build of Ripple.
You can also adjust your own framework by [adding/removing modules](https://github.com/rijs/minimal/blob/master/src/index.js#L1-L11).
## Docs
See [rijs/docs](https://github.com/rijs/docs) for more guides, index of modules, API reference, etc