What goes well with Bun? Hotdogs! ๐ญ๐ญ๐ญ
Hotdogjs is a LiveView web framework optimized to run on Bun.
Hotdogjsโs key features are:
Github repo: https://github.com/floodfx/hotdogjs
๐ Feedback is a gift! ๐ Please ask questions, report issues, give feedback, etc by opening an issue. I love hearing from you!
At a high level, LiveViews automatically detect registered events (clicks, form updates, etc.) and routes these events (and metadata) from client to server over a websocket. When the server receives an event, it routes it to a developer defined handler which can update the server state and kicks off a re-render. The server then calculates a view diff and sends this diff back to the client which is automatically applied to the DOM.
All of the complicated stuff (communication protocol, websocket lifecycle, state management, diff calculation and application, etc.) is handled automatically by Hotdogjs. The developer only needs to focus on the business logic of their application using the simple, yet powerful programming paradigm of LiveViews.
bun create hotdogjs
# (follow the prompts)
cd my-hotdogjs-app
bun install
bun dev
Open http://localhost:3000 in your browser.
Thanks to DenverScript for the invite and great community!
git clone https://github.com/floodfx/hotdogjscd hotdogjs && bun installcd packages/examples && bun devpublic/ - Location of static assets like client-side javascriptviews/ - Hotdogjs LiveViews routed based on File System Router; all .ts files in this directory are expected to be LiveViewslayouts/ - Template for laying out the Viewspackage.json - Node.js compatible package.json with required dependencies and default scriptshotdogjs-conf.toml - (optional) hotdogjs configuration file for overriding default configurationA View is defined by creating a file in the views/ directory. We use Bunโs File System Router to route requests to the appropriate View and extract path and query parameters that are passed to the View as params in the mount method. You can override the directory that the Views are located in by setting the viewsDir option in the hotdogjs-conf.toml file.
A View is a web page that responds to events, updates its state, and generates HTML diffs. Views are initially rendered as HTML over HTTP. Once rendered, the View automatically connects to the server via a websocket. When connected, the View automatically sends events from the client and receives then automatically applies diffs to the DOM. You should extend BaseView to create your own View.
View have the following API:
mount is called once before the View is rendered. It is useful for setting up initial state based on request parameters and/or loading data from the server.handleParams is called when the URL changes and the View is already mounted. This method is useful for updating the state of the View based on the new URL including the query parameters and/or route parameters.handleEvent is called when an event is received from the client or server. This method is useful for updating the state of the View based on the event and is the main way to handle user interactions or server-based events with the View.render defines the HTML to render for the View based on the current state. This method is called once when the View is mounted and again whenever the View state changes.shutdown is called when the View is being shutdown / unmounted. This method is useful for cleaning up any resources that the View may have allocated.layoutName is an optional property that can be used to specify the layout to use for the View. If not specified, the View will use the default.html layout in the layouts/ directory.There are many more examples in the packages/examples directory but just to get a feel for LiveViews, here is a simple counter example:
export default class Increment extends BaseView<AnyEvent> {
count: number = 0;
handleEvent(ctx: ViewContext, event: AnyEvent) {
if (event.type === "inc") this.count++;
}
render() {
return html`
<h3>${this.count}</h3>
<button hd-click="inc">+</button>
`;
}
}
There are four main types of user events that user can trigger:
You can add the following attributes to your HTML elements to send events (and custom data) to the server based on user interactions:
| Binding | Attribute | Supported |
|---|---|---|
| Custom Data | hd-value-* |
โ |
| Click Events | hd-click |
โ |
| Click Events | hd-click-away |
โ |
| Form Events | hd-change |
โ |
| Form Events | hd-submit |
โ |
| Form Events | hd-feedback-for |
โ |
| Form Events | hd-disable-with |
โ |
| Form Events | hd-trigger-action |
โ |
| Form Events | hd-auto-recover |
โ |
| Focus Events | hd-blur |
โ |
| Focus Events | hd-focus |
โ |
| Focus Events | hd-window-blur |
โ |
| Focus Events | hd-window-focus |
โ |
| Key Events | hd-keydown |
โ |
| Key Events | hd-keyup |
โ |
| Key Events | hd-window-keydown |
โ |
| Key Events | hd-window-keyup |
โ |
| Key Events | hd-key |
โ |
| DOM Patching | hd-update |
โ |
| DOM Patching | hd-remove |
โ |
| Client JS | hd-hook |
โ |
| Rate Limiting | hd-debounce |
โ |
| Rate Limiting | hd-throttle |
โ |
| Static Tracking | hd-track-static |
โ |
Components are small, reusable pieces of UI that can be used across multiple Views. You should put components somewhere outside of the views/ directory so they arenโt routed to accidentally. Components can be stateless or stateful and encapsulate their own state in the latter case. Components are rendered using the component method which takes a Component class and returns an instance of that class.
Components have the following API:
mount is called once before the Component is rendered. It is useful for setting up initial state based on parameters passed to the Component from the View. Components with an id property are stateful (regardless of if you are actually using state) and those without an id are stateless.update is called prior to the render method for both stateful and stateless Components. This method is useful for additional business logic that needs to be executed prior to rendering the Component.handleEvent if your Component is stateful (i.e. it has an id property), this method must be implemented and is called when an event is received from the client or server. This method is useful for updating the state of the Component based on the event and is the main way to handle user interactions or server-based events with the Component.render defines the HTML to render for the Component based on the current state. This method is called once when the Component is mounted and again whenever the Component state changes (if it is stateful).shutdown is called when the Component is being shutdown / unmounted. This method is useful for cleaning up any resources that the Component may have allocated.User events (clicks, etc) typically trigger a server-side event which updates the state and re-renders the HTML. Sometimes you want to update the DOM without (or in addition to) initiating a server round trip. This is where JS Commands come in.
JS Commands support a number of client-side DOM manipulation function that can be used to update the DOM without a server round trip. These functions are:
add_class - Add css classes to an element including optional transition classesremove_class - Remove css classes from an element including optional transition classestoggle_class - Toggle a css class on an element including optional transition classesset_attribute - Set an attribute on an elementremove_attribute - Remove an attribute from an elementtoggle_attribute - Toggle an attribute on an elementshow - Show an element including optional transition classeshide - Hide an element including optional transition classestoggle - Toggle the visibility of an elementdispatch - Dispatch a DOM event from an elementtransition - Apply transition classes to an element (i.e., animate it)push - Push an event to the LiveView server (i.e., trigger a server round trip)navigate - Navigate to a new URLpatch - Patch the current URLfocus - Focus on an elementfocus_first - Focus on the first child of an elementpop_focus - Pop the focus from the elementpush_focus - Push the focus to the elementexec - Execute a function at the attribute locationJS Commands are used in the render function as part of the HTML markup:
render() {
return <div hd-click="${new JS().push("increment")}">Increment</div>
}
JS Commands are โchainableโ (i.e., fluent) so you can chain multiple commands together as needed and they will be executed in the order they are called:
render() {
return <div hd-click="${new JS().hide().push("increment")}">Increment and hide</div>
}
When using the bun create hotdogjs command, the generated project comes with a eject script that, when run, will generate the basic server configuration, client-side javascript file, hotdogjs-conf.toml, and update the package.json. This gives you the ability to customize the project to your liking including full control over the http server and client-side javascript file.
Hotdogjs supports both server rendering and client interactivity in a single programming model called LiveView. Hotdogjs is built on the following principles:
If you want/need to override the default configuration of a Hotdogjs project, you can do so by creating a hotdogjs-conf.toml file in the root of your project.
The following configuration options are supported:
publicDir - location of the public directory defaults to public/layoutsDir - location of the layouts directory (defaults to layouts/)clientFile - location of the client-side javascript file (defaults to empty which uses the default client-side javascript file)clientDir - location of the client-side javascript file (defaults to empty which uses the default client-side javascript file)skipBuildingClientJS - whether the server should NOT build the client-side javascript file on startup (defaults to false)viewsDir - where to find the views (defaults to views/)staticPrefix - the prefix for static assets (defaults to /static)staticExcludes - a comma separated list of static assets to exclude from the build (defaults to empty)wsBaseUrl - the base url for the websocket connection (defaults to empty which uses the default websocket url)Alternatively, you can override the default configuration by setting the following environment variables:
HD_PUBLIC_DIR - location of the public directory defaults to public/HD_LAYOUTS_DIR - location of the layouts directory (defaults to layouts/)HD_CLIENT_JS_FILE - location of the client-side javascript file (defaults to empty which uses the default client-side javascript file)HD_CLIENT_JS_DEST_DIR - location of the client-side javascript file (defaults to empty which uses the default client-side javascript file)HD_SKIP_BUILD_CLIENT_JS - whether the server should NOT build the client-side javascript file on startup (defaults to false)HD_VIEWS_DIR - where to find the views (defaults to views/)HD_STATIC_PREFIX - the prefix for static assets (defaults to /static)HD_STATIC_EXCLUDES - a comma separated list of static assets to exclude from the build (defaults to empty)HD_WS_BASE_URL - the base url for the websocket connection (defaults to empty which uses the default websocket url)p element in Tree array diffsBun is fast as heck and provides some powerful features out-of-the-box that Hotdogjs takes advantage of. We specifically lean into Bun features on purpose since this project seeks to take advantage of Bun in its entirety. Below is a list of Bun features that are used in Hotdogjs:
I wrote LiveViewJS and got annoying to abstract it in such a way to support Node and Deno. Bun was intriguing because if its speed and native Typescript support. Hotdogjs started as a way to learn more about Bun and re-implement LiveViews for the Bun runtime and based on other LiveView implementations Iโd learned from along the way. Overall, the implementation and APIs are similar with lots of simplifications in Hotdogjs to make it more ergonomic and more powerful with almost zero dependencies.
I also wrote or helped write the following LiveView frameworks: