A port of Phoenix LiveView to Typescript/Javascript

Overview

LiveViewJS

Front-end framework for back-end developers

Credit πŸ™Œ

This is a backend implementation of Phoenix LiveView in Typescript. What the Phoenix folks have built is phenominal and I wanted to use that paradigm in Typescript and make it available to others.

Quick Overview of LiveView Approach

How Phoenix desribes LiveView:

LiveView is an exciting new library which enables rich, real-time user experiences with server-rendered HTML. LiveView powered applications are stateful on the server with bidrectional communication via WebSockets, offering a vastly simplified programming model compared to JavaScript alternatives.

In other words, LiveView takes a very different approach than the popular SPA frameworks like React, Vuejs, and Svelt to building rich, highly interactive web applications. Instead of sending down a bundle of javascript, LiveView apps render an HTML page on the first request and then connect via a persistent socket over which client events are sent and updated received. These events may trigger a state update on the server and a re-calculation of what the page should look like. Instead of reloading the page, the client receives a "diff" of the page via the socket and the page's DOM is updated. (This video by Pragmatic Studio does an amazing job of explaining how LiveView works.)

The programming paradigm is extremely powerful and productive!

Feedback is a 🎁

I am not an expert on Phoenix Liveview, Elixir, Erlang VMs, etc or really most things. Please feel free to open an issue with questios, bugs, etc.

Status - Ξ²

LiveViewJS is in Ξ²eta. The project is still young but the code is stable, tested, and well-documented.

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.

Usage - Show me some code! ⌨️

Step 0 Install LiveViewJS npm i liveviewjs

Step 1 Implement a LiveViewComponent

implements LiveViewComponent , LiveViewExternalEventListener { // mount is called before html render on HTTP requests and // when the socket is connected on the phx-join event mount(params: LiveViewMountParams, session: Partial , socket: LiveViewSocket ) { // set the default value(s) for the component data return { brightness: 10 }; }; // Define and render the HTML for your LiveViewComponent // This function is called after any context change and // only diffs are sent back to the page to re-render render(context: LightContext) { const { brightness } = context; return html`

Front Porch Light

${brightness}%
` }; // Handle events sent back from the client... Events // may update the state (context) of the component and // cause a re-render handleEvent(event: LightEvent, params: never, socket: LiveViewSocket ) { const ctx: LightContext = { brightness: socket.context.brightness }; switch (event) { case 'off': ctx.brightness = 0; break; case 'on': ctx.brightness = 100; break; case 'up': ctx.brightness = Math.min(ctx.brightness + 10, 100); break; case 'down': ctx.brightness = Math.max(ctx.brightness - 10, 0); break; } return ctx; } }">
import { SessionData } from "express-session";
import {html, BaseLiveViewComponent, LiveViewComponent, LiveViewExternalEventListener, LiveViewMountParams, LiveViewSocket } from "liveviewjs";

// define your component's data shape
export interface LightContext {
  brightness: number;
}

// define the component events
export type LightEvent = "on" | "off" | "up" | "down";

// implement your component
export class LightLiveViewComponent extends BaseLiveViewComponent<LightContext, never> implements
  LiveViewComponent<LightContext, never>,
  LiveViewExternalEventListener<LightContext, LightEvent, never> {

  // mount is called before html render on HTTP requests and
  // when the socket is connected on the phx-join event
  mount(params: LiveViewMountParams, session: Partial<SessionData>, socket: LiveViewSocket<LightContext>) {
    // set the default value(s) for the component data
    return { brightness: 10 };
  };

  // Define and render the HTML for your LiveViewComponent
  // This function is called after any context change and
  // only diffs are sent back to the page to re-render
  render(context: LightContext) {
    const { brightness } = context;
    return html`
    <div id="light">
      <h1>Front Porch Light h1>
      <div class="meter">
        <div>${brightness}%div>
        <progress id="light_level" value="${brightness}" max="100">
        progress>
      div>

      <button phx-click="off">
        Off
      button>

 <button phx-click="down">
 Down
 button>   <button phx-click="up">  Up  button>   <button phx-click="on">  On  button>  div>  ` }; // Handle events sent back from the client... Events // may update the state (context) of the component and // cause a re-render handleEvent(event: LightEvent, params: never, socket: LiveViewSocket<LightContext>) { const ctx: LightContext = { brightness: socket.context.brightness }; switch (event) { case 'off': ctx.brightness = 0; break; case 'on': ctx.brightness = 100; break; case 'up': ctx.brightness = Math.min(ctx.brightness + 10, 100); break; case 'down': ctx.brightness = Math.max(ctx.brightness - 10, 0); break; } return ctx; } }

Step 2 - Register your LiveViewComponents and start the HTTP and Socket server with LiveViewServer

// import package
import {LiveViewServer} from "liveviewjs";

// create new LiveViewServer
const lvServer = new LiveViewServer();

// define your routes by mapping paths to LiveViewComponents
const lvRouter: LiveViewRouter = {
  "/light": new LightLiveViewComponent();
}
// AND then passing the router to the server
lvServer.registerLiveViewRoutes(lvRouter);

// OR register your route with the server directly
lvServer.registerLiveViewRoute("/light", new LightLiveViewComponent());

// then start the server
lvServer.start();

Other features to be implemented:

NPM Commands

npm i - install the deps

npm run build - builds the framework, client, and examples (server)

npm run watch - continually rebuilds the codebase when files are updated

npm run examples - runs the examples [See src/examples]

npm run test - runs the (few) tests

Run and Browse Examples

Credit: These examples are adapted from an amazing Phoenix Video / Code Course authored by the folks at Pragmatic Studio.

Navigate to src/examples to see the example code.

Run npm run examples then point your browser to:

  • http://localhost:4444/ - Shows the index of all the examples

There is also a standalone TodoMVC example application written in LiveViewJS.

More Details on the Approach to Building LiveViewJS πŸ“

  • Reuse Phoenix Client Libraries and app.js code - The Phoenix team has done a ton of heavy lifting on the client that we can just use. We also benefit from fixes and improvements in the future. [See src/client/liveview.ts for client code.]

  • Reuse Phoenix socket message protocol - The Phoenix team already figured out a great protocol to send events to/from the server. We just implemented a different backend.

  • Follow component API design (i.e. mount, render etc), reimplemented with Typescript (so even more type-safe) - Components in LiveViewJS follow the mount, render, handleEvent, and handleInfo API defined in Phoenix. Again, no need to invent a new API.

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!

Comments
  • Deno Example Error

    Deno Example Error

    Hey are the docs missing a build step for deno?

    LiveViewJS Express is listening on port 9001!
    GET http://localhost:9001/ - 2022-10-09T05:08:00.606Z
    GET http://localhost:9001/favicon.ico - 2022-10-09T05:08:01.161Z
    GET http://localhost:9001/counter - 2022-10-09T05:08:09.683Z
    GET http://localhost:9001/js/index.js - 2022-10-09T05:08:09.703Z
    [uncaught application error]: NotFoundError - No such file or directory (os error 2), stat 'public/js/index.js'
    
    request: { url: "http://localhost:9001/js/index.js", method: "GET", hasBody: false }
    response: { status: 404, type: undefined, hasBody: false, writable: true }
    
        at createHttpError (https://deno.land/x/[email protected]/httpError.ts:128:10)
        at send (https://deno.land/x/[email protected]/send.ts:279:13)
        at async file:///Users/thomasreggi/Documents/GitHub/liveviewjs/packages/deno/src/example/index.ts:82:3
        at async dispatch (https://deno.land/x/[email protected]/middleware.ts:41:7)
        at async dispatch (https://deno.land/x/[email protected]/middleware.ts:41:7)
        at async dispatch (https://deno.land/x/[email protected]/middleware.ts:41:7)
        at async file:///Users/thomasreggi/Documents/GitHub/liveviewjs/packages/deno/src/deno/server.ts:117:11
        at async dispatch (https://deno.land/x/[email protected]/middleware.ts:41:7)
        at async file:///Users/thomasreggi/Documents/GitHub/liveviewjs/packages/deno/src/example/index.ts:58:3
        at async dispatch (https://deno.land/x/[email protected]/middleware.ts:41:7)
    

    nothing happens when I click the counter buttons

    Screen Shot 2022-10-09 at 1 10 02 AM

    opened by reggi 4
  • Simple example on deno-deploy

    Simple example on deno-deploy

    I think a simple example on deno-deploy would be really cool, right now there's just a lot of boiler-plate to think about. I also think that using JSX could would be cool as well just wrapping the render method so something else can create the HTML string like https://hdot.dev/ or preact (https://www.npmjs.com/package/preact-render-to-string).

    import { createLiveView, html } from "liveviewjs";
    
    /**
     * A basic counter that increments and decrements a number.
     */
    export const counterLiveView = createLiveView<
      { count: number }, // Define LiveView Context (a.k.a state)
      { type: "increment" } | { type: "decrement" } // Define LiveView Events
    >({
      mount: (socket) => {
        // init state, set count to 0
        socket.assign({ count: 0 });
      },
      handleEvent: (event, socket) => {
        // handle increment and decrement events
        const { count } = socket.context;
        switch (event.type) {
          case "increment":
            socket.assign({ count: count + 1 });
            break;
          case "decrement":
            socket.assign({ count: count - 1 });
            break;
        }
      },
      render: (context) => {
        // render the view based on the state
        const { count } = context;
        return html`
          <div>
            <h1>Count is: ${count}</h1>
            <button phx-click="decrement">-</button>
            <button phx-click="increment">+</button>
          </div>
        `;
      },
    });
    
    serve({
      "/": counterLiveView,
    })
    
    opened by reggi 3
  • Support async LiveViewComponentManager methods

    Support async LiveViewComponentManager methods

    Hey Donnie,

    First of all, thanks for putting this together. Love it.

    While playing around with the examples (and inspecting the code for the Todo MVC demo), I noticed there are no examples which fetch data externally via async/fetch operations. What's the recommended/planned way of doing so?

    I feel as if mount, render, handleEvent and handleInfo should all optionally support async signatures, so that one can actually fetch/push data from/to external sources/apis.

    I'm new to Liveview (thanks for referencing the pragmatic series), so still trying to wrap my head around some of the more advanced concepts.

    Once again, thanks for bringing this to node.

    opened by juanpprieto 3
  • Question: Where is BroadcastChannelPubSub used?

    Question: Where is BroadcastChannelPubSub used?

    The file here with the BroadcastChannelPubSub export, where is this used in the deno example?

    https://github.com/floodfx/liveviewjs/blob/main/packages/deno/src/deno/broadcastChannelPubSub.ts

    opened by reggi 2
  • broken link in readme

    broken link in readme

    The autocomplete example in readme is linking to a 404 page: https://github.com/floodfx/liveviewjs/blob/main/packages/examples/src/liveviews/autocomplete

    opened by beenotung 1
  • Optimize Phoenix Diff Replies

    Optimize Phoenix Diff Replies

    Currently the diff replies are sending all the dynamic and static parts of the live views instead of only sending the parts that have changed. That is, if only the 4th dynamic part has changed then the diff should only contain that key/value pair.

    opened by floodfx 1
  • Name conflict

    Name conflict

    It appears the LiveView name is already in use via the Phoenix LiveView project. Will you consider changing the name so that users of both libraries can avoid confusion and get better search results?

    opened by bbhoss 0
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • 08e4e41: Fix unsubscribe bug with undefined subscriber

    @liveviewjs/[email protected]

    Patch Changes

    @liveviewjs/[email protected]

    Patch Changes

    opened by github-actions[bot] 0
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • 6318d39: Fix bug where handleInfo was not being awaited

    @liveviewjs/[email protected]

    Patch Changes

    @liveviewjs/[email protected]

    Patch Changes

    opened by github-actions[bot] 0
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • c6f6985: Support Node16+ (add polyfills for fetch and structuredClone)

    @liveviewjs/[email protected]

    Patch Changes

    • c6f6985: Support Node16+ (add polyfills for fetch and structuredClone)
    • Updated dependencies [c6f6985]

    @liveviewjs/[email protected]

    Patch Changes

    • c6f6985: Support Node16+ (add polyfills for fetch and structuredClone)
    • Updated dependencies [c6f6985]
    opened by github-actions[bot] 0
  • Version Packages

    Version Packages

    This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

    Releases

    [email protected]

    Patch Changes

    • a927c16: Support path params in route
      • Core: Support path params in routes for HTTP and WS
      • Examples: Add "helloNameLiveView" example
      • Express / Deno: Update server integration to use matchRoute helper

    @liveviewjs/[email protected]

    Patch Changes

    • a927c16: Support path params in route
      • Core: Support path params in routes for HTTP and WS
      • Examples: Add "helloNameLiveView" example
      • Express / Deno: Update server integration to use matchRoute helper
    • Updated dependencies [a927c16]

    @liveviewjs/[email protected]

    Patch Changes

    • a927c16: Support path params in route
      • Core: Support path params in routes for HTTP and WS
      • Examples: Add "helloNameLiveView" example
      • Express / Deno: Update server integration to use matchRoute helper
    • Updated dependencies [a927c16]
    opened by github-actions[bot] 0
  • handleInfo() is not run timely when called from callbacks

    handleInfo() is not run timely when called from callbacks

    I cannot get handleInfo() to run automatically when called from a callback (e.g. establishing a database connection).

    I am using liveview 0.7.5. and node with express.

    Putting this in mount():

    setTimeout(()=>socket.sendInfo("from mount->setTimeout"),0);
    

    Results in sendInfo() receiving the message after the next handleEvent().

    export const myLiveView = createLiveView({
        mount: (socket) => {
            socket.assign({
                useEmoji: false,
            });
            setTimeout(()=>socket.sendInfo("from mount->setTimeout"),0);
        },
        handleEvent(event, socket) {
            socket.assign({ useEmoji: !socket.context.useEmoji });
        },
        handleInfo: (info, socket) => {
            console.log({info});
        },
        render: (context /* const */) => {
            const msg = context.useEmoji ? "hello" : "Hello World";
            
            return html`
                ${msg}
                <br/>
                <button phx-click="toggle">Toggle message</button>
                `;
        }
    });
    
    opened by trondsg 3
Owner
Donnie Flood
Donnie Flood
Banana for Solr - A Port of Kibana

Banana The Banana project was forked from Kibana, and works with all kinds of time series (and non-time series) data stored in Apache Solr. It uses Ki

Lucidworks 668 Dec 13, 2022
A tiny TypeScript library for 2D vector math.

Vecti A tiny TypeScript library for 2D vector math. Documentation Features ?? Addition, subtraction, multiplication and division ✨ Dot, cross and Hada

Jan MΓΌller 17 Nov 30, 2022
A TypeScript library designed to help with making Beat Saber modcharts

Welcome to ReMapper! This is a TypeScript library designed to help with making Beat Saber modcharts. Here are some notable features: Wrappers for Note

Swifter 37 Dec 28, 2022
JavaScript 3D library.

three.js JavaScript 3D library The aim of the project is to create an easy to use, lightweight, cross-browser, general purpose 3D library. The current

Mr.doob 87.9k Jan 2, 2023
Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser

Fabric.js Fabric.js is a framework that makes it easy to work with HTML5 canvas element. It is an interactive object model on top of canvas element. I

Fabric.js 23.6k Jan 3, 2023
Babylon.js is a powerful, beautiful, simple, and open game and rendering engine packed into a friendly JavaScript framework.

Babylon.js Getting started? Play directly with the Babylon.js API using our playground. It also contains a lot of samples to learn how to use it. Any

Babylon.js 19.1k Jan 4, 2023
The JavaScript library for modern SVG graphics.

Snap.svg Β· A JavaScript SVG library for the modern web. Learn more at snapsvg.io. Follow us on Twitter. Install Bower - bower install snap.svg npm - n

Adobe Web Platform 13.6k Dec 30, 2022
Highcharts JS, the JavaScript charting framework

Highcharts JS is a JavaScript charting library based on SVG, with fallbacks to VML and canvas for old browsers. Official website: www.highcharts.com D

Highsoft 10.9k Jan 9, 2023
The Swiss Army Knife of Vector Graphics Scripting – Scriptographer ported to JavaScript and the browser, using HTML5 Canvas. Created by @lehni & @puckey

Paper.js - The Swiss Army Knife of Vector Graphics Scripting If you want to work with Paper.js, simply download the latest "stable" version from http:

Paper.js 13.5k Dec 30, 2022
JavaScript Vector Library

RaphaΓ«l: Cross-browser vector graphics the easy way Visit the library website for more information: http://raphaeljs.com https://dmitrybaranovskiy.git

Dmitry Baranovskiy 11.2k Jan 3, 2023
A JavaScript library dedicated to graph drawing

sigma.js - v1.2.1 Sigma is a JavaScript library dedicated to graph drawing, mainly developed by @jacomyal and @Yomguithereal. Resources The website pr

Alexis Jacomy 10.3k Jan 3, 2023
JavaScript diagramming library for interactive flowcharts, org charts, design tools, planning tools, visual languages.

GoJS, a JavaScript Library for HTML Diagrams GoJS is a JavaScript and TypeScript library for creating and manipulating diagrams, charts, and graphs. S

Northwoods Software Corporation 6.6k Dec 30, 2022
mxGraph is a fully client side JavaScript diagramming library

NOTE 09.11.2020 : Development on mxGraph has now stopped, this repo is effectively end of life. Known forks: https://github.com/jsGraph/mxgraph https:

JGraph 6.5k Dec 30, 2022
Attractive JavaScript charts for jQuery

flot About flot is a JavaScript plotting library for engineering and scientific applications derived from Flot: http://www.flotcharts.org/ Take a look

Flot 5.9k Dec 22, 2022
JavaScript toolkit for creating interactive real-time graphs

Rickshaw Rickshaw is a JavaScript toolkit for creating interactive time series graphs, developed at Shutterstock Table of Contents Getting Started Ins

Shutterstock 6.5k Dec 28, 2022
πŸ”₯ JavaScript Library for HTML5 canvas based heatmaps

heatmap.js Dynamic Heatmaps for the Web. How to get started The fastest way to get started is to install heatmap.js with bower. Just run the following

Patrick Wied 5.9k Jan 2, 2023
Cubism.js: A JavaScript library for time series visualization.

Cubism.js Cubism.js is a D3 plugin for visualizing time series. Use Cubism to construct better realtime dashboards, pulling data from Graphite, Cube a

Square 4.9k Jan 3, 2023
Create word clouds in JavaScript.

Word Cloud Layout This is a Wordle-inspired word cloud layout written in JavaScript. It uses HTML5 canvas and sprite masks to achieve near-interactive

Jason Davies 3.6k Jan 2, 2023
Open Source Javascript Gantt

Frappe Gantt A simple, interactive, modern gantt chart library for the web View the demo Β» Install npm install frappe-gantt Usage Include it in your

Frappe 3.5k Dec 30, 2022