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/hotdogjs
cd hotdogjs && bun install
cd packages/examples && bun dev
public/
- 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 View
s 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. View
s 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 View
s. 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
. Component
s 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 Component
s. 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: