-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from floodfx/frameworkify
Frameworkify
- Loading branch information
Showing
33 changed files
with
706 additions
and
3,072 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,144 @@ | ||
## LiveViewJS Proof of Concept | ||
## LiveViewJS | ||
|
||
Learned about [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) and really loved the concept. Wanted to learn more about how it worked and see if I could get a basic PoC working in Typescript. | ||
⌘ *Front-end framework for back-end developers* ⌘ | ||
|
||
### To Run | ||
### Credit 🙌 | ||
This is a port of [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) to Typescript / Javascript. What the Phoenix folks have built is phenominal and I wanted to use that paradigm in Typescript and make it available to others. | ||
|
||
### Approach 📐 | ||
|
||
* **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. | ||
|
||
### Status - **⍺** | ||
This is still in very early PoC territory. You probably shouldn't put this into production just yet. | ||
|
||
### Show me some code! ⌨️ | ||
**Step 1** Implement a `LiveViewComponent` | ||
```ts | ||
import escapeHtml from "../server/templates"; | ||
import { LiveViewComponent, LiveViewContext, LiveViewExternalEventListener, LiveViewInternalEventListener } from "../server/types"; | ||
import { PhxSocket } from "../server/socket/types"; | ||
|
||
// define your component's data shape | ||
export interface LightContext { | ||
brightness: number; | ||
} | ||
|
||
// optionally define the events your component will respond to | ||
export type LightEvent = "on" | "off"; | ||
|
||
// simple in memory database for the context | ||
// production apps would use more durable system | ||
const _db: { [key: string]: LightContext } = {}; | ||
|
||
// implement your LiveViewComponents | ||
export class LightLiveViewComponent implements | ||
LiveViewComponent<LightContext>, | ||
LiveViewExternalEventListener<LightContext, "on", any>, | ||
LiveViewExternalEventListener<LightContext, "off", any> { | ||
|
||
|
||
// handle mount events - called on initial http request AND subsequent socket connections | ||
mount(params: any, session: any, socket: PhxSocket) { | ||
const ctx: LightContext = { brightness: 10 }; | ||
_db[socket.id] = ctx; // store the ctx by socket id to look up later | ||
return { data: ctx }; | ||
}; | ||
|
||
// define and render the HTML for your LiveViewComponent | ||
render(context: LiveViewContext<LightContext>) { | ||
// the `escapeHtml` function is a tagged template literal that | ||
// allows LiveView to send back only the data that has changed | ||
// based on user events - note the `phx-click` bindings on the | ||
// buttons in the template | ||
return escapeHtml` | ||
<div id="light"> | ||
<h1>Front Porch Light</h1> | ||
<div class="meter"> | ||
<span style="width: ${context.data.brightness} %>%"> | ||
${context.data.brightness}% | ||
</span> | ||
</div> | ||
<button phx-click="off"> | ||
Off | ||
</button> | ||
<button phx-click="on"> | ||
On | ||
</button> | ||
</div> | ||
` | ||
}; | ||
|
||
// handle external events sent form client | ||
handleEvent(event: LightEvent, params: any, socket: PhxSocket) { | ||
const ctx = _db[socket.id]; // lookup current context by socket id | ||
switch (event) { | ||
case 'off': | ||
ctx.brightness = 0; | ||
break; | ||
case 'on': | ||
ctx.brightness = 100; | ||
break; | ||
} | ||
// udpate context by socket id | ||
_db[socket.id] = ctx; | ||
return { data: ctx }; | ||
} | ||
|
||
} | ||
``` | ||
|
||
**Step 2** - Register your `LiveViewComponent`s and start the HTTP and Socket server with `LiveViewServer` | ||
```ts | ||
// 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(); | ||
``` | ||
|
||
### Welcome your feedback, issues, and PRs | ||
I am not an expert on Phoenix Liveview, Elixir, Erlang VMs, etc or most things. Please feel free to open an issue with questios, bugs, etc. | ||
|
||
### Commands | ||
`npm i` - install the deps | ||
|
||
`npm run server-build` - compile the server and watch for changes (tsc) | ||
`npm run build` - builds the framework, client, and examples (server) | ||
|
||
`npm run client-build` - compiles the client code (parcel) | ||
`npm run watch` - continually rebuilds the codebase when files are updated | ||
|
||
`npm run server` - runs the server | ||
`npm run examples` - runs the examples [See `src/examples`] | ||
|
||
`npm run test` - runs the (few) tests | ||
|
||
### To see | ||
Point your browser to `http://localhost:3002/` | ||
### See Examples | ||
**Credit** These examples are adapted from an amazing [Phoenix Video / Code Course](https://online.pragmaticstudio.com/courses/liveview-starter/steps/15) authored by the folks at [Pragmatic Studio](https://pragmaticstudio.com/). | ||
|
||
Run `npm run examples` then point your browser to: | ||
* `http://localhost:4444/light` - control a fictional porch light with some buttons [See `src/examples/light_liveview.ts`] | ||
* `http://localhost:4444/license` - calculate the per seat costs to license a SaaS product [See `src/examples/license_liveview.ts`] | ||
* `http://localhost:4444/sales-dashboard` - show the aggregated sales stats updated every second [See `src/examples/sales_dashboard_liveview.ts`] | ||
* `http://localhost:4444/search` - search for business by zip code (e.g. 80204) [See `src/examples/live-search/*`] | ||
* `http://localhost:4444/autocomplete` - search for businesses by city with automatically suggested names [See `src/examples/autocomplete/*`] | ||
|
||
### Where is the example page from? | ||
Watched the free version of this course: [Phoenix LiveView Course](https://pragmaticstudio.com/phoenix-liveview) | ||
### Thanks! | ||
Thanks to [@ogrodnek](https://github.com/ogrodnek) for the early support, feedback, and the idea to reuse the Phoenix client code instead of reinventing! |
Oops, something went wrong.