An isomorphic runtime for functional JavaScript programs.
- Inspired by Elm. Based on concepts similar to Elm’s – in good ol’ JavaScript – with server-side support.
- Pure. Write view components using pure functions. Handle side-effects in separate handler functions.
- Typed. Written in TypeScript.
runtime
separates application logic between the browser, worker and server.
Scope | Responsibility |
---|---|
Browser | Responsible for updating the DOM and using other Web APIs such as Animation, History, Geolocation, and so on. |
Worker | Responsible for keeping track of application state, virtual DOM and diffing. The worker also handles network requests. |
Server | Responsible for rendering the initial state of a program, given a set of input properties such as URL path, query parameters, cookies and so on. The server also handles network requests. |
Using a worker thread for maintaining application state and diffing the virtual DOM, keeps heavy operations away from the main UI thread. The main UI thread should mostly be idle, and sometimes perform DOM operations and interact with other Web APIs.
First install the dependencies:
npm install @transclusion/vdom -S
npm install @transclusion/runtime-core -S
npm install @transclusion/runtime-browser -S
npm install @transclusion/runtime-server -S
npm install @transclusion/runtime-worker -S
Components in runtime
are called programs (like in Elm).
/** @jsx createVElement */
import {createVElement} from '@transclusion/vdom'
// Expose message/command types that will be sent to the browser
export const ports = {
history: ['history/POP_STATE', 'history/PUSH_STATE', 'history/REPLACE_STATE']
}
// Initialize the program’s model value
export function init(props) {
return [{path: props.path, state: props.state, title: props.title}, null]
}
// Update the program’s model value (after which the view is re-rendered)
export function update(model, msg) {
switch (msg.type) {
case 'history/POP_STATE':
case 'history/PUSH_STATE':
case 'history/REPLACE_STATE':
return [{...model, path: msg.path, state: msg.state, title: msg.title}, null]
default:
return [model, null]
}
}
function navView() {
return (
<div>
<a href="/" on={{click: {type: 'history/PUSH_STATE', path: '/', preventDefault: true}}}>
Home
</a>{' '}
<a href="/blog" on={{click: {type: 'history/PUSH_STATE', path: '/blog', preventDefault: true}}}>
Blog
</a>
</div>
)
}
// Render the view as virtual DOM
export function view(model) {
switch (model.path) {
case '/':
return (
<div>
{navView()}
<h1>Home screen</h1>
</div>
)
case '/blog':
return (
<div>
{navView()}
<h1>Blog screen</h1>
</div>
)
default:
return (
<div>
{navView()}
<h1>Not found: {model.path}</h1>
</div>
)
}
}
import {run} from '@transclusion/runtime-browser'
const rootElm = document.getElementById('root')
run(
{
element: rootElm.firstChild,
props: __INITIAL_PROPS__,
worker: new Worker('worker.js')
},
({ports}) => {
ports.history.subscribe(msg => {
switch (msg.type) {
case 'history/PUSH_STATE':
history.pushState(msg.state, msg.title, msg.path)
break
case 'history/REPLACE_STATE':
history.replaceState(msg.state, msg.title, msg.path)
break
}
})
window.addEventListener('popstate', event => {
ports.history.send({
type: 'history/POP_STATE',
path: location.pathname,
state: event.state,
title: document.title
})
})
}
)
import {run} from '@transclusion/runtime-worker'
import * as root from './root'
run({
program: root,
scope: this
})
import {run} from '@transclusion/runtime-server'
import express from 'express'
import * as root from './root'
const app = express()
app.get('/*', (req, res) => {
run({
program: root,
props: {path: req.path}
})
.then(context => {
res.send(`<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="root">${context.html}</div>
<script>
var __INITIAL_PROPS__ = ${JSON.stringify(context.model)}
</script>
<script src="browser.js"></script>
</body>
</html>`)
})
.catch(err => {
res.status(500)
res.send(err.stack)
})
})
app.listen(3000)