liveviewjs
liveviewjs copied to clipboard
LiveView-based library for reactive app development in NodeJS and Deno
π 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 downloading huge JS bundles, "hydration", JSX, etc
- User-experiences as rich, reactive, and dynamic as SPA frameworks but with a much simpler developer paradigm that is requires less context switching
- Super-simple LiveView lifecycle that can be learned in 5 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 handles sending events to the server over a web socket and automatically appling diffs 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 the effort, variants, workarounds, hacks, etc) - just render some HTML and CSS, add some LiveView attributes, and ship it!
- Small yet extensive user-events system that enables rich, dynamic user experiences: clicks, form events, key events, and focus/blur events, and escape hatches if needed (but most of the time, you don't need them)
- 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
Canonical "Counter" Example in LiveViewJS
import { createLiveView, html } from "liveviewjs";
/**
* A basic counter that increments and decrements a number.
*/
export const counterLiveView = createLiveView<
{ count: number }, // Define LiveView Context / State
{ type: "increment" } | { type: "decrement" } // Define LiveView Events
>({
// Setup / initialize the LiveView Context (i.e. set count to 0)
mount: (socket) => {
socket.assign({ count: 0 });
},
// Handle incoming increment and decrement events from User input
handleEvent: (event, socket) => {
const { count } = socket.context;
switch (event.type) {
case "increment":
socket.assign({ count: count + 1 });
break;
case "decrement":
socket.assign({ count: count - 1 });
break;
}
},
// Renders the Counter View based on the current Context / State
render: async (context) => {
const { count } = context;
return html`
<div>
<h1>Count is: ${count}</h1>
<button phx-click="decrement">-</button>
<button phx-click="increment">+</button>
</div>
`;
},
});
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 javascript-based 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 should be able to 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. See the section Adding LiveViewJS to your existing app below for more details.
Run the LiveViewJS Examples in 30 seconds
# clone the LiveViewJS repo
git clone [email protected]:floodfx/liveviewjs.git
If you want to run examples for NodeJS (on Express), you can do it like this:
cd packages/express
npm install
npm run start
# open your browser to http://localhost:4001/
If you want to run examples for Deno, you can do it like this:
cd packages/deno
deno run --allow-net --allow-read --allow-write --allow-env --import-map=import_map.json src/example/index.ts
# open your browser to http://localhost:9001/
To see the examples you just ran navigate to the examples folder:
cd ../examples
Anatomy of a LiveView
The LiveViewJS API is extremely simple but very flexible. There are 5 methods that make up the LiveView lifecycle: mount
, handleParams
, handleEvent
, handleInfo
, and render
. The render
method is the only "required" method. The other methods are optional but typically mount
and handleEvent
are also defined in order to setup the context (i.e. state) of the LiveView and handle user input.
-
mount
is called both when theLiveView
is rendered for the HTTP request and upon the first time theLiveView
is mounted (i.e. connected) via the websocket. This is where you should load data and set the initial context of theLiveView
-
handleParams
is called on initial loading of theLiveView
(one-time, aftermount
) as well as on events that manipulate the URL of theLiveView
. This is where you should handle any context (i.e. state) changes that are based on theLiveView
's URL parameters. -
handleEvent
is called when events are initiated by the user interactions with theLiveView
. Things like "clicks", "key events", "form input", and "focus/blur" events are all handled by this method. More details on "bindings" for user events below. -
handleInfo
handles server-side events which we call "info" that are initiated fromhandleEvent
or other pub/sub subscriptions. Asynchronous processes are often sent tohandleInfo
via ahandleEvent
(e.g. run a search query). More details below. -
render
is the only required method which provides the HTML and CSS that is rendered to the client. All of the other methods manupulate the "context" (i.e. state) of theLiveView
and the resulting context is passed to therender
method to determine the HTML and CSS that is rendered to the client.
HTTP Lifecycle vs Websocket Lifecycle
Quick note on HTTP and Websocket Lifecycles. In LiveViewJS, each URL path is a different LiveView. When a user visits a URL, the browser sends a HTTP get request to the server and LiveViewJS renders the page over HTTP. This is the first HTTP lifecycle for that LiveView instance and the appropriate methods are called on the LiveView which are typically mount
, handleParams
, and render
. After the page fully loads in the browser, LiveViewJS automatically connects to the server over a websocket and runs the same initial lifecycle methods for the Websocket lifecycle and then starts to handle any user or server ininitated events. The major difference is between the HTTP and Websocket initialization is that there is no user event handling or internal info handling for the HTTP lifecycle. This entire HTTP + WS lifecycle happens extremely fast as again, we are rendering HTML not downloading large JS bundles or doing any other heavy lifting.
User Events
There are 4 main types of user events that a LiveView can listen to and respond to:
- Click events
- Form events
- Key events
- Focus events
To listen for user events there are a set of "bindings" (a.k.a. attributes) that you add to the HTML elements in your LiveView
returned by the render
method.
Click Events
User clicks are the most common type of user event and there are two types of click bindings:
-
phx-click
- Add this binding to an HTML element (e.g.<... phx-click="myEvent" ...>
) and when a user clicks on the element the event (i.e. value of the attribute) will be sent to the server. -
phx-click-away
- This binding is similar tophx-click
except that an event will occur when the user clicks outside of the element.
Click binding example - send the increment
event to the server when the user clicks on the "+" button
<button phx-click="increment">+</button>
Form Events
Form events are triggered by the user interacting with form inputs. There are two types of form bindings:
-
phx-change
- When a user changes the value of a form element, the event named by thephx-change
attribute will be sent to the server along with all the form values. This is typically used for form validation purposes prior to form submission. -
phx-submit
- This binding is initiated when a user submits a form and the event named by thephx-submit
attribute will be sent to the server along with all the form values.
Forms are typically used in conjunction with LiveViewChangeset
s to provide validation rules (based on zod) and various template helpers like form_for
, text_input
, error_tag
. These are designed to work together to make form validation and submission easy and powerful. We'll dive into more details later on. For now here is an example of a form with phx-change
and phx-submit
bindings:
<form action="#" phx-change="validate" phx-submit="save">
...
</form>
Key Events
Key events are triggered by the user pressing a key on the keyboard. There are key bindings for both the element-level and the window-level:
-
phx-keydown
,phx-window-keydown
- When a user presses a key down on the keyboard, the event named by the attribute will be sent to the server along with the key that was pressed. -
phx-keyup
,phx-window-keyup
- When a user releases a key on the keyboard, the event named by the attribute will be sent to the server along with the key that was released.
phx-key
is an optional attribute which limits triggering of the key events to the key provided in the attribute (e.g. phx-key="ArrowUp"
). You can find a list of the keys on MDN Keyboard Values.
Key binding example - send the key_event
event to the server along with the {key: "ArrowUp"}
payload when the user presses the "ArrowUp" key
```html
<div phx-window-keydown="key_event" phx-key="ArrowUp" />
Focus Events
If a DOM element emits focus and blur events, you can use the following bindings to react to those events:
-
phx-focus
- When a user focuses on an element, the event named by thephx-focus
attribute will be sent to the server. -
phx-blur
- When a user blurs from an element, the event named by thephx-blur
attribute will be sent to the server. Similar to key events, there are window-level and element-level bindings: -
phx-window-focus
- When a user focuses on the window, the event named by thephx-window-focus
attribute will be sent to the server. -
phx-window-blur
- When a user blurs from the window, the event named by thephx-window-blur
attribute will be sent to the server.
Focus binding example - send the focus_event
event to the server when the user focuses on the input and the blur_event
event when the user blurs from the input
<input name="text" phx-focus="focus_event" phx-blur="blur_event"/>
Additional Bindings
There are other bindings that provide additional functionality for your LiveView and work in conjunction with the event bindings we reviewed above.
Value Bindings
When you need to send along additional data with an event binding you can use a value binding which looks something like phx-value-[NAME]
where [Name]
is replaced by the key of the value. This binding can be used in conjunction with other click, key, and focus bindings.
Value binding example - send the mark_complete
event to the server along with the {id: "myId"}
payload when the user clicks on the "Complete" button
<button phx-click="mark_complete" phx-value-id="myId">Complete</button>
Note the [NAME]
part of phx-value-[NAME]
is used as the object key while the attribute value is used as the object value.
Rate Limiting Bindings
Deboucing and throttling events is a very common need and to support these use-cases there are the following bindings:
-
phx-debounce
- Debounce an event by the number of milliseconds specified in the attribute value or by setting the value toblur
. This is useful for preventing multiple events from being sent to the server in rapid succession. Whenblur
is the value, the event will be debounced until the element is blurred by the user. Typically used for input elements. -
phx-throttle
- Throttle an event by the number of milliseconds specified in the attribute value. In contrast to debouncing, throttling emits an event immediately and then only once every specified number of milliseconds. Typically used to rate limit click events, key events, and mouse actions.
Debounce binding example - send the validate
event to the server when a user blurs away from the address input
<form action="#" phx-change="validate" phx-submit="save">
<!--// only send "validate" event when address input is blurred -->
<input name="address" phx-debounce="blur" />
Debounce binding example - send the search
event to the server 1 second after a user stops typing
<form action="#" phx-change="search">
<!--// send "search" event after 1 second of debouncing -->
<input name="query" phx-debounce="1000" />
Throttling binding example - only send one volume_up
event every 500ms
<!--// rate limit clicks on a volume up event -->
<button phx-click="volume_up" phx-throttle="500" />
LiveView Socket
The LiveViewSocket
object is a major part of the LiveViewJS API and LiveView lifecycle. It is passed into each of the non-render
lifecycle methods (mount
, handleParams
, handleEvent
, handleInfo
) and provides functionality to manipulate the state of the LiveView, send messages internally, subscribe to pub/sub topics, and many other useful functionality. Here is some of the functionality provided by the LiveViewSocket
:
Updating State
socket.assign
and socket.context
are the work-horse methods for manipulating and reading the state of the LiveView. The assign
method is used to update the state of the LiveView and the context
property is used to read the state of the LiveView.
// Update the context (i.e. current state) of the `LiveView`
socket.assign({ foo: "bar" })
// use the context from the socket elsewhere
if(socket.context.foo === "baz") {
// do something
}
// or
const { foo } = socket.context;
When creating a LiveView
developers can provide a type annotation for TContext
which describes the "shape" of the context. e.g.
// You can define the "shape" of the TContext by annotating the createLiveView function
const myLiveView = createLiveView<{foo: string}>(
mount: (socket) => {
socket.assign({ foo: "bar" });
...
socket.assign({ baz: "qux" }); // type error no "baz" property in context
}
...
)
// Alternatively, you can define the context type first and then use it to as a type annotation for the `LiveView`
// Define the MyContext interface
interface MyContext { foo: string };
// Annotate the createLiveView function with the MyContext interface
const myLiveView = createLiveView<MyContext>(...)
The state of a LiveView
is persisted across page loads on the server-side (in memory by default). For this reason, there is a method called socket.tempAssign
which allows a developer to tell LiveViewJS to reset a context property to a given value after the render lifecycle. Typically this is used for large objects or collections that don't change often and therefore probabaly don't need to be stored in memory (e.g. collection of users or messages, etc).
// first assign a large object to the context
socket.assign({ photos: [
...// 10s, 100s, 1000s, of photos
]});
// use tempAssign to tell LiveViewJS to clear the photos array after this render cycle
socket.tempAssign({ photos: [] });
Send "Internal" Info
socket.sendInfo
enables a LiveView
to send message to itself which is useful for executing actions that are asynchronous. Messages sent via socket.sendInfo
are received by the handleInfo
method after the render lifecycle has completed. (In other words, handleInfo
is called after the render
call which will result in another render
after handleInfo
completes.) A typical pattern for events that run asynchronous processes is to show a loading indicator when the user initiates the event, then execute the process in the background, and update the UI when the process finishes.
When creating your LiveView
you can provide the typing for TInfo
which describes the "shape" of the possible info messages. e.g.
// Define the MyContext, MyEvents, and MyInfo types
type MyContext = {query: string, loading: boolean, results: string[]};
type MyEvents = {type: "search", query: string};
type MyInfo = {type: "run_search", query: string} | {type: "refresh"};
// Annotate the createLiveView function with the types
const myLiveView = createLiveView<MyContext, MyEvents, MyInfo>(
handleEvent: (event, socket) => {
...
if(event.type === "search" ) {
// update the context with loading status and empty results so
// that UI will be updated for user
socket.assign({ loading: true, results: [], query: event.query });
// send internal message to run the search process
socket.sendInfo({ type: "run_search", query: event.query })
}
}
...
handleInfo: (info, socket) => {
if(info.type === "run_search") {
const { query } = info;
// run the search
const results = searchService.run(query)
// update the context with results which will update the UI
socket.assign({ loading: false, results })
}
...
}
...
)
socket.sendInfo
can just take a type as a string for cases where there isn't additional information passed along with the message.
// or send just the "type" as a string
socket.sendInfo("refresh");
Push Methods
There are various methods for "pushing" from the server to the client.
-
socket.pushPatch
is used to push an update of the browser URL (i.e. the path and parameters) from the server -
socket.pushRedirect
can be used to shutdown the currentLiveView
and load anotherLiveView
without a full HTML refresh or can be used to reload the currentLiveView
if need be -
socket.pushEvent
can be used to send data to a client "Hook" (see below) which can be used to update client-side state / UI without reloading. This is useful for libraries that themselves render client-side such as charting or mapping libraries.
Page Title and Flash Messages
socket.pageTitle
updates the html <title>
tag for the current page. This is useful for changing the title as needed. It requires the use of the live_title
html helper in your LiveViewPageRenderer
.
socket.putFlash
Displaying messages at the top of a page to the user is often called "flash". putFlash
adds a string key/value pair to the SessionData
object available in your LiveViewRootRenderer
where you can access it and render the messages as you see fit. Adding a phx-click="lv:clear-flash"
attribute to your "flash" component will automtically clear the flash message (i.e. remove the key/value pair from the SessionData
object and initiate a re-render).
Repeating / Intervals
socket.repeat
takes a void function and the intervalMS which to repeat it and continuously runs that function until the LiveView
is closed. Useful for kicking off a repeating process like polling for new data, refreshing a chart, etc.
Subscribing to Pub/Sub Topics
socket.subscribe
creates a subscription to a given topic. Info published on that topic will be passed to the handleInfo
method of the subscribing LiveView
. This is an easy way to listen for events like object mutations, chat messaging, and any other real-time events that may interest your users. More on Pub/Sub below.
Pub/Sub
LiveView uses the "Pub/Sub" model underneath to process and deliver messages. Pub/Sub is short for "Publisher" / "Subscriber" in which a service that wants to publish can create a topic and other services that want to listen to events on that topic can subscribe.
Subscribing
socket.subscribe
takes a string which is the name of the topic to which you are subscribing this LiveView. When you subscribe to a topic, updates to that topic are sent to the handleInfo
method of your LiveView.
const myLiveView = createLiveView(
mount: async (socket) => {
// typically want to check the websocket is connected, that is,
// this isn't an http request
if (socket.connected) {
// listen for events on the "my_stuff" topic
await socket.subscribe("my_stuff");
}
},
...
handleInfo: (info, socket) => {
// handle "my_stuff" events
if()
...
}
);
Broadcasting
To broadcast to a topic, you call the broadcast
method on the implementation (see below) of the PubSub provider. Publishing can and does happen outside of the LiveView implementation for example as part of a data source class or otherwise where it makes sense for your use case.
import { SingleProcessPubSub } from 'liveviewjs';
// or import { RedisPubSub } from '@liveviewjs/express
// or import { BroadcastChannelPubSub } from '@liveviewjs/deno`
...
// setup your client if necessary (Redis details for example)
const pubSub = new SingleProcessPubSub();
...
// publish: in this case the "some_event" to the "my_stuff" topic
pubSub.publish("my_stuff", { type: "some_event", foo: "bar" });
See the Volunteers
example for a more complete showcase.
Configuring Pub/Sub Implementation
LiveViewJS provides three different implementations out of the box for Pub/Sub: SingleProcessPubSub
, RedisPubSub
, and BroadcastChannelPubSub
. SingleProcessPubSub
is shipped as part of the liveviewjs
core library. RedisPubSub
is shipped with the @liveviewjs/express
library and BroadcastChannelPubSub
is part of the Deno library. SingleProcessPubSub
only supports Pub/Sub within a single process and is built on top of the EventEmitter
APIs. RedisPubSub
and BroadcastChannelPubSub
enable developers to support Pub/Sub across multiple server instances.
Part of configuring the server is passing in an instance of the Pub/Sub implementation:
// Use the SingleProcessPubSub
const liveViewServer = new NodeExpressLiveViewServer(
...
new SingleProcessPubSub(),
...
);
...
// Alternatively use a different implementation
const redisPubSub = new RedisPubSub({
//config
})
const liveViewServer = new NodeExpressLiveViewServer(
...
redisPubSub,
...
);
Client-side Javascript
LiveViewJS pages require some client-side javascript to be loaded in the HTML page to parse the phx-*
attributes, connect to the server (via websocket), apply the diffs, and handle user interactions.
Default Client-side JS via CDN:
You can load the default LiveViewJS client-side by adding the following to your LiveViewJS template:
<script defer type="text/javascript" src="https://cdn.jsdelivr.net/gh/floodfx/[email protected]/packages/examples/dist/liveviewjs-examples.browser.js">
</script>
Customizing client-side JS:
The default typescript for the client-side javascript is the following:
import NProgress from "nprogress";
import { Socket } from "phoenix";
import "phoenix_html";
import { LiveSocket } from "phoenix_live_view";
// Define the route that websockets will use to connect to your server
const url = "/live";
// Pull out the csrf token from the meta tag
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
// Create the LiveSocket
let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken } });
// Show progress bar on live navigation and form submits (requires NProgress css)
window.addEventListener("phx:page-loading-start", (info) => NProgress.start());
window.addEventListener("phx:page-loading-stop", (info) => NProgress.done());
// connect if there are any LiveViews on the page
liveSocket.connect();
// If you want to expose liveSocket messages in the console for debugging, uncomment the following:
// liveSocket.enableDebug();
// If you want to simulate request latency, you can uncomment the following
// liveSocket.enableLatencySim(1000)
// finally add the liveSocket to the window
(window as any).liveSocket = liveSocket;
"Hooks" (not the React kind) for anything else
Sometimes you need to do something that is not supported by any of the existing user event bindings or that requires hooking into a client event. LiveView has "Hooks" for these types of situations.
Note: The term "Hooks" comes from Phoenix/LiveView which this project is based on and whose client library we are built on. It is in no way related to React Hooks. It is unfortunate that "Hooks" is overloaded but we don't find it very confusing considering how different LiveView is from React.
File Upload Events
Work in progress!
Lots of other LiveView examples
We have lots of 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.
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="https://cdn.deno.land/liveviewjs/versions/0.3.0/raw/packages/examples/dist/liveviewjs-examples.browser.js"></script>
<!-- nprogress shows a tiny progress bar when requests are made between client/server -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/nprogress.css" />
<!-- your favorite css library -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/[email protected]/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
-
Updating HTML Document
<title />
- LiveViewJS Changesets.
- Temporary Assigns.
- Routing Details.
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!