FaC: Das alte Flag von Terminen, die in die Zeiterfassung übernommen worden sind, wird ebenfalls berücksichtigt.
385 lines
11 KiB
Markdown
385 lines
11 KiB
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
|