Skip to content
/ morph Public

Embeddable fullstack library designed for creating Hypermedia-Driven Applications without a build step, based on HTMX and Hono

License

Notifications You must be signed in to change notification settings

vseplet/morph

Repository files navigation

Morph

Morph mascot

JSR GitHub commit activity GitHub last commit

👋 ATTENTION!

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.

Core principles:

  • 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

Get started

(see full examples)

Add packages

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

Make main.ts and add imports

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";

Create simple page (for all runtimes)

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));

Setup server

Deno

Deno.serve(app.fetch);

Bun

export default app;

Node

serve(app);

And run

Deno
deno -A main.ts

Bun
bun main.ts

Node
node --experimental-strip-types main.ts

Documentation

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.

Templates

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

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>
  `
);

Client-Side JavaScript

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>
`;

Styles

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>
`;

Routing, pages and Hono

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

Layout

[Coming soon]

Wrapper

[Coming soon]

Meta

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.

Partial and HTMX

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 ...

RPC

(Coming soon)

Conclusion

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

License

MIT

About

Embeddable fullstack library designed for creating Hypermedia-Driven Applications without a build step, based on HTMX and Hono

Topics

Resources

License

Stars

Watchers

Forks