This package is under development and will be frequently updated. The author would appreciate any help, advice, and pull requests! Thank you for your understanding 😊
Morph is an embeddable fullstack library for building Hypermedia-Driven Applications without a build step, based on HTMX and Hono.
Morph exists for one purpose: to simplify the creation of small, straightforward interfaces while eliminating the need to separate frontend and backend into distinct services (as commonly done with Vue/Nuxt and React/Next). It's perfect for embedding web interfaces into existing codebases, whether that's admin panels, dashboards for CLI utilities, Telegram Web Apps (and similar platforms), or small applications and pet projects.
Morph requires virtually nothing - it doesn't impose project structure, doesn't force you to "build" or compile anything. It just works here and now. Morph is ideal for solo developers who don't have the resources to develop and maintain a separate frontend, or simply don't need one. In this sense, Morph is the antithesis of modern frameworks, adapted for working with Deno, NodeJS, and Bun.
- Each component can call its own API that returns hypertext (other components)
- All components are rendered on the server and have access to server-side context
- Components can be rendered and re-rendered independently
- Components form a hierarchy, can be nested in one another, and returned from APIs
- Minimal or no client-side JavaScript
- No build step
- No need to design API data structures upfront
- The library can be embedded into any Deno/Node/Bun project
Deno
deno add jsr:@vseplet/morph jsr:@hono/hono
Bun
bunx jsr add @vseplet/morph
bun add hono
Node
npx jsr add @vseplet/morph
npm i --save hono @hono/node-server
First, import Hono based on your runtime:
Deno
import { Hono } from "@hono/hono";
Bun
import { Hono } from "hono";
Node
import { serve } from "@hono/node-server";
import { Hono } from "hono";
Then, add Morph imports (same for all runtimes):
import { component, fn, html, js, meta, morph, styled } from "@vseplet/morph";
const app = new Hono()
.all("/*", async (c) =>
await morph
.page(
"/",
component(async () =>
html`
${meta({ title: "Hello, World!" })}
<h1>Hello, World!</h1>
<pre class="${styled`color:red;`}">${(await (await fetch(
"https://icanhazdadjoke.com/",
{
headers: {
Accept: "application/json",
"User-Agent": "My Fun App (https://example.com)",
},
},
)).json()).joke}</pre>
${fn(() => alert("Hello!"))}
`
),
)
.fetch(c.req.raw));
Deno
Deno.serve(app.fetch);
Bun
export default app;
Node
serve(app);
Deno
deno -A main.ts
Bun
bun main.ts
Node
node --experimental-strip-types main.ts
Key points to understand:
Morph uses Hono under the hood for routing, middleware functions, and more. All routes are described using Hono's routing syntax.
In Morph, everything consists of components that return template literals. The
templates are described using the html
tagged template literal:
htmlsome html here
.
All components, templates, and other elements are rendered on the server upon request. In the future, the rendering results of pages and individual components may be cacheable.
All components in Morph are functions that return template literals. Here's a simple example:
html`
<h1>Hello World<h1>
`
Template literals are flexible and support all JavaScript template literal features, including nested templates:
const buttonName = "Click Me"
html`
<h1>Hello World<h1>
${html`
<button>${buttonName}</button>
`}
`
They can also include functions (including asynchronous ones) that return templates:
const buttonName = "Click Me"
html`
<h1>Hello World<h1>
${async () => {
// some async code here
return html`
<p>And here's some data</p>
`
}}
`
Components are the building blocks of Morph applications. They are functions (possibly asynchronous) that accept props and return template literals. Pages themselves are also components.
Here's a simple component example:
const cmp = component(
async () =>
html`
<div>
<p>Hello, World</p>
</div>
`,
);
Components can accept typed props that are defined using TypeScript generics:
component<{ title: string }>(
async (props) =>
html`
<h1>${props.title}</h1>
`,
);
Besides user-defined props, components have access to default props defined in
the MorphPageProps type, including: request: Request
and
headers: Record<string, string>
. This provides immediate access to request
headers, parameters, and other request details during component rendering.
Components can be composed together:
const h1 = component<{ title: string }>((props) =>
html`
<h1>${props.title}</h1>
`
);
const page = component(() =>
html`
<page>
${h1({ title: "Hello, World" })}
</page>
`
);
And they support array operations for dynamic rendering:
const h1 = component<{ title: string }>((props) =>
html`
<h1>${props.title}</h1>
`
);
const page = component(() =>
html`
<page>
${["title 1", "title 2"].map((title) => h1({ title }))}
</page>
`
);
You can embed JavaScript code that will run on the client side directly in your templates. Here's a simple example:
html`
<div>
<p id="title">Hello, World</p>
${js`document.querySelector('#title').innerHTML = 'LoL';`}
</div>
`;
This code will be wrapped in an anonymous function and added to the page's
<body>
right after the main HTML content.
Additionally, you can define a function that will be transpiled to a string and inserted into the page code in a similar way:
html`
<div>
<p id="title">Hello, World</p>
${fn(() => document.querySelector("#title").innerHTML = "LoL")}
</div>
`;
Not everything is convenient to describe in separate .css files or (especially) inline through style=... In some cases, it might be more convenient to generate an entire class at once, and for this purpose, we have the following approach:
const color = "#0056b3";
const buttonStyle = styled`
border-radius: 15px;
border: 1px solid black;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: ${color};
}
`;
html`
<div>
<button class="${buttonStyle}">Click Me</button>
</div>
`;
The entry point is the morph
object, which provides methods for creating pages
and handling their rendering on request:
const website = morph
.page("/a", cmpA)
.page("/b", cmpB);
Then, using Hono, you can create an application and start serving it:
const app = new Hono()
.all("/*", async (c) => website.fetch(c.req.raw));
// Start the server (implementation varies by runtime)
Deno.serve(app.fetch); // for Deno
// export default app; // for Bun
// serve(app); // for Node.js
[Coming soon]
[Coming soon]
A simple mechanism that allows you to set template values from any component. For example, to set the page title:
const cmp = component(
async () =>
html`
${meta({
title: "Hello, World!"
})}
<div>
<p>Hello, World</p>
</div>
`
);
You can also add content to the head or body sections:
meta({
head: `<link rel="stylesheet" href="styles.css">` // add CSS
bodyEnd: `<script>alert("Hi!")</script>`
})
Additionally, it allows you to set HTTP headers, status codes, and other response metadata.
HTMX is a powerful library that enables moving data handling and page/component updates from JavaScript to HTML, seamlessly integrating with HTML syntax. In Morph, you can re-render individual components without reloading the entire page (the component is rendered on the server).
Here's a simple example (full):
const cmp = component(async (props) => html`
<div ${props.hx()} hx-swap="outerHTML" hx-trigger="every 1s">
${Math.random()}
</div>
`);
Note the props.hx()
function - it returns a path that can be used to trigger the component's re-rendering. For more information about hx-swap
and hx-trigger
attributes, please refer to the official HTMX documentation.
To enable component re-rendering, you need to explicitly register it with the Hono router:
morph
.partial(cmp)
// .page()
// .fetch ...
(Coming soon)
This project is currently in the prototyping stage. Many things may change, be added, or removed as we work towards the main project goals. I welcome any help and contributions:
- Test the library and report issues
- Study the documentation and suggest improvements
- Submit pull requests with your changes
- Share your ideas and feedback
- Use Morph in your pet projects
Feel free to reach out to me on Telegram for any questions or discussions: @vseplet