UNPKG

rijs

Version:
385 lines (262 loc) 11.3 kB
# 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) => { .. }) ``` &nbsp; ## 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). &nbsp; ## 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() ``` &nbsp; ## 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', []) } ``` &nbsp; ## 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. &nbsp; ## Events Dispatch an event on the root element to communicate changes to parents (`node.dispatchEvent`). &nbsp; ## 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). &nbsp; ## 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. &nbsp; ## 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. &nbsp; ## 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. &nbsp; ## 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)) &nbsp; ## 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. &nbsp; ## 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. &nbsp; ## 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). &nbsp; ## Docs See [rijs/docs](https://github.com/rijs/docs) for more guides, index of modules, API reference, etc