liveviewjs v0.10.4
π LiveViewJS
An anti-SPA, HTML-first, GSD-focused library for building LiveViews in NodeJS and Deno
LiveView Paradigm
The LiveView model is simple. When a user makes an HTTP request, the server renders an HTML page. That page then connects to the server via a persistent web socket. From there, user-initiated events (clicks, form input, key events, focus/blur events) are sent over the web socket to the server in very small packets. When the server receives the events, it runs the business logic for that LiveView, calculates the new rendered HTML, and then sends only the diffs to the client. The client automatically updates the page with the diffs. The server can also send diffs back to the client based on events on the server or received from other clients (think chat, or other pub/sub scenarios).
This paradigm was invented by the developers of the Phoenix Framework and is widely used (and battle-tested) by tens of thousands of Elixir developers and projects. LiveViewJS is an implementation of the Phoenix backend in Typescript / JavaScript. For the client-side code, we use the exact same code/libraries that Phoenix uses.
What are the advantages of LiveView?
- Blazing fast first paint - we are just rendering HTML, no huge JS bundles, "hydration", tree-shaking, JSX, etc
- User-experiences as rich, reactive, and dynamic as SPA frameworks but with a much simpler developer experience with less code
- Super-simple LiveView lifecycle that can be learned in 10 minutes - usually just
mount
,handleEvent
,render
and sometimeshandleParams
andhandleInfo
- No need to build a separate back-end REST and/or GraphQL API and related shenanigans - the library automatically sent to the server over a web socket and diffs are automatically applied to the client
- No synchronizing state between front-end and back-end - all the state is where your data lives...on the server
- No need to reinvent routing - LiveViews are just URLs and the browser knows how to route them already
- No need to build or learn a component library (with all its gotchas, workarounds, etc) - just render some HTML and CSS, add some LiveView attributes, and receive events
- Small yet extensive user-events system that enables rich, dynamic user experiences: clicks, form events, key events, and focus/blur events
- Robust, battle-tested browser libraries used by tens of thousands of applications - we use the Phoenix LiveView javascript libraries directly (no reinventing the wheel)
- Simple to use beyond "toy" examples - complexity does not grow exponentially like SPA frameworks
Standard "Hello World" Counter Example in LiveViewJS
Below is the standard "hello world" counter implemented in LiveViewJS.
// define the shape of your LiveView's data
interface Context {
count: number;
}
// define events handled by this LiveView
type Events = { type: "increment" } | { type: "decrement" };
// create a LiveView using the Context and Events defined above
export class CounterLiveView extends BaseLiveView<Context, Events> {
// handle the inital request
mount(params: LiveViewMountParams, session: Partial<SessionData>, socket: LiveViewSocket<Context>): void {
socket.assign({ count: 0 }); // initialize your state
}
// update state based on events
handleEvent(event: Events, socket: LiveViewSocket<Context>) {
const { count } = socket.context;
switch (event.type) {
case "increment":
socket.assign({ count: count + 1 });
break;
case "decrement":
socket.assign({ count: count - 1 });
break;
}
}
// re-render the LiveView when state changes
render(context: Context, meta: LiveViewMeta) {
const { count } = context;
return html`
<div>
<h1>Count is: ${count}</h1>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
</div>
`;
}
}
Other Examples
We have over a dozen other, non-trivial examples of LiveViews in the packages/examples
directory including:
- XKCD - Browse the latest XKCD comics
- Dashboard - A Dashboard that updates every second with random metrics
- Volume Control - Volume Control with keyboard inputs (no actual sound)
- Search - Search for businesses in a city by zip code (try 80204)
- Autocomplete - Autocomplete for businesses in a city by zip code (try 80204)
- Sorting - A table that is sortable by clicking on the column headers and supports pagination
- and many more...
You can run these by checking out this repo and navigating to either the packages/express
or packages/deno
directory and following the directions in the README.md there.
You can also install the examples in your NodeJS app by running:
npm i -D @liveviewjs/examples
Check out the code in the packages/express
directory for example code.
For Deno, the examples are available on DenoLand: (replace VERSION below with the latest version of this library)
https://deno.land/x/liveviewjs@VERSION/packages/examples/mod.ts
Check out the code in the packages/deno
directory for example code.
How to use LiveViewJS in your NodeJS or Deno app
LiveViewJS works on both NodeJS and Deno and can be added to your application one route at a time on any web server. Currently, we have prebuilt integrations (HTTP middleware and websocket adaptors) for NodeJS+ExpressJS (see: packages/express
) and Deno+Oak (see: packages/deno
). LiveViewJS is designed so that any NodeJS or Deno webserver that supports HTTP middleware and web sockets can use it (e.g. Koa, Hapi, etc). If you want to use LiveViewJS on a different webserver please open an issue and we'll work with you to add support for it.
Adding LiveViewJS to your existing app
Prerequisites to adding LiveViewJS
- Install LiveViewJS in your NodeJS or Deno app
- NodeJS:
npm i liveviewjs
- Deno: Add liveviewjs to your
deps.ts
orimport_map.json
- https://deno.land/x/liveviewjs@VERSION/mod.ts (replace VERSION with the latest version of this library)
Quick Integration Walkthrough
Quick start of adding LiveViewJS to your application:
- Create one or more
LiveView
s (useBaseLiveView
as your base class) - Feel free to use an example or include from the@liveviewjs/examples
package.
export class MyLiveView extends BaseLiveView<MyContext, MyEvents> {...}
- Create a
LiveViewRouter
to map yourLiveView
s to request paths. This is how requests are routed to yourLiveView
s both HTTP and WebSockets.
const liveViewRouter: LiveViewRouter = {
"/myroute": new MyLiveView(), // maps /myroute to MyLiveView
}
- Define a
LiveViewPageRenderer
which defines the page layout in which yourLiveView
s will be rendered. Optionally, you can define aLiveViewRootRenderer
which defines another level in which to render yourLiveView
s (often used for things like flash messages)
// define the page layout in which your LiveViews will be rendered,
// also loads the LiveView client javascript which facilitates the
// communication between the client and the server
export const pageRenderer: LiveViewPageRenderer = (
pageTitleDefaults: PageTitleDefaults,
csrfToken: string,
liveViewContent: LiveViewTemplate
): LiveViewTemplate => {
return html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- the csrfToken is required for security and will be provided to this function -->
<meta name="csrf-token" content="${csrfToken}" />
<!-- live_title_tag enables title updates from LiveViews -->
${live_title_tag(pageTitle, { prefix: pageTitlePrefix, suffix: pageTitleSuffix })}
<!-- your browser/liveview javascript see: packages/browser-->
<script defer type="text/javascript" src="/client-liveview.js"></script>
<!-- nprogress shows a tiny progress bar when requests are made between client/server -->
<link rel="stylesheet" href="https://unpkg.com/nprogress@0.2.0/nprogress.css" />
<!-- your favorite css library -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css" />
</head>
<body>
<!-- the to-be-rendered LiveView content -->
${safe(liveViewContent)}
</body>
</html>`
}
- Configure your
LiveViewServerAdaptor
and integrate thehttpMiddleware
andwsAdaptor
functions into your server.
// initialize the LiveViewServerAdaptor for your server type
const liveViewAdaptor = new NodeExpressLiveViewServer(
router,
new NodeJwtSerDe(signingSecret),
new SingleProcessPubSub(),
pageRenderer,
{ title: "Express Demo", suffix: " Β· LiveViewJS" },
new SessionFlashAdaptor(),
// optional: rootRenderer
);
//...
// setup the LiveViewJS middleware
app.use(liveViewAdaptor.httpMiddleware());
//...
// integrate LiveViewJS with this server's websocket listener
const wsRouter = liveViewAdaptor.wsRouter();
// send websocket requests to the LiveViewJS message router
wsServer.on("connection", (ws) => {
const connectionId = nanoid();
ws.on("message", async (message) => {
// pass websocket messages to LiveViewJS
await wsRouter.onMessage(connectionId, message.toString(), new NodeWsAdaptor(ws));
});
ws.on("close", async () => {
// pass websocket close events to LiveViewJS
await wsRouter.onClose(connectionId);
});
});
That's it!!! Start your server and start making requests to the LiveView routes!
Feedback is a π
Like all software, this is a work in progress. If you have any feedback, please let us know by opening an issue on the GitHub repository.
Status - Ξ²
LiveViewJS is in Ξ²eta. The project is still young but the code is tested and well-documented. We are looking for feedback and contributions.
For Elixir/Phoenix Folks these are the Implemented Phoenix Bindings
The bindings below marked with β
are working and tested and most of them have example usage in the examples
codebase. Those with ?
, I have not gotten around to testing so not sure if they work. Those marked with β are not yet implemented and known not to work.
(See Phoenix Bindings Docs for more details)
Binding | Attribute | Supported |
---|---|---|
Params | phx-value-* | β |
Click Events | phx-click | β |
Click Events | phx-click-away | β |
Form Events | phx-change | β |
Form Events | phx-submit | β |
Form Events | phx-feedback-for | β |
Form Events | phx-disable-with | β |
Form Events | phx-trigger-action | οΉ |
Form Events | phx-auto-recover | οΉ |
Focus Events | phx-blur | β |
Focus Events | phx-focus | β |
Focus Events | phx-window-blur | β |
Focus Events | phx-window-focus | β |
Key Events | phx-keydown | β |
Key Events | phx-keyup | β |
Key Events | phx-window-keydown | β |
Key Events | phx-window-keyup | β |
Key Events | phx-key | β |
DOM Patching | phx-update | β |
DOM Patching | phx-remove | οΉ |
JS Interop | phx-hook | β |
Rate Limiting | phx-debounce | β |
Rate Limiting | phx-throttle | β |
Static Tracking | phx-track-static | β |
LiveViewJS Changesets
Phoenix's Ecto ORM library and Phoenix LiveView rely on Ecto Changesets to allow filtering, validation, and other logic to be applied to the data. Changesets are a powerful way to apply logic to data and are used in Phoenix's ORM and LiveView. LiveViewJS uses Changesets to provide a similar API to Phoenix's though it is NOT a full-blown ORM.
Detailed documentation on LiveViewJS Changesets.
Additional Feature Documentation
Credit π
Huge shout out to the folks behind Phoenix! They are visionaries and I am just trying to expand their influence to the Typescript / Javascript ecosystem.
Gratitude π
Thanks to @ogrodnek for the early support, feedback, and the idea to reuse the Phoenix client code instead of reinventing!
Thanks to @blimmer for the awesome feedback, documentation suggests, and support!
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago