Bruh Docs

(This is a work in progress)

See the project on Github, the code is fairly small. If you need anything, the discussions are open!

Why You Should Care

Bruh is a simple and solid library made to build anything for the web. You get a simple API to both generate HTML and dynamically build the DOM at basically no cost. It has a very small code size (0 if you use prerendering) and is extremely performant. Everything about it is optimized for simplicity (no magic) and flexibility - so you can be creative! No matter what you build - a tiny one-off static web page or a complex web app - bruh aims to be simpler, more performant, and more flexible than other libraries like React.

In Abstract

This library's design is derived from these needs:

  • Manipulate the DOM using modern and fast browser API's

  • Dynamically create interfaces with the DOM

  • Deliver arbitrary "prerendered" HTML first

  • "Hydrate" this HTML in arbitrary ways

  • Do all of this in a unified and highly composable way

  • Also supply convenience functions separately from the base

So the typical way to use bruh would look like:

  1. Make a web page as a component

  2. Render that component as a string of HTML and serve that (write the string to a file to statically serve it or stream it if it is dynamically prerendered)

  3. Load a javascript entry point that contains code which "hydrates" the HTML

If you decide to skip #3, then bruh can function as a library that simply builds fully static web pages. If you want to make a fully-fledged web app, then this process is open to extreme flexibility.

A Tour

A quick note on following along - try out code blocks in your browser's console (in an empty tab like about:blank). You only need to replace in the form of :

import { ... } from "bruh/dom/html"
// to
const { ... } = await import("https://unpkg.com/bruh/src/dom/html.mjs")

Making Elements

A function named e (for "element") is one very important piece of the puzzle. Here is an example of its usage:

import { e } from "bruh/dom/meta-node"

const component =
  e("div")(
    e("p")("bruh is neat"),
    e("p")(
      "it is ", e("strong")("very"), " simple"
    )
  )

document.body.append(component.toNode())

If the above code were to be run on a blank page, it would result in this DOM structure:

<!doctype html>
<html>
  <head></head>
  <body>
    <div>
      <p>bruh is neat</p>
      <p>it is <strong>very</strong> simple</p>
    </div>
  </body>
</html>

Basic Prerendering

Of course, writing e("") all of the time gets annoying, so individual wrappers are available. Here is a nodejs script that prerenders the same above HTML and saves it as a file:

import {
  html, head, body,
  div, p, strong
} from "bruh/dom/html"

import { writeFile } from "fs/promises"


const component =
  div(
    p("bruh is neat"),
    p(
      "it is ", strong("very"), " simple"
    )
  )

const prerendered =
  "<!doctype html>" +
  html(
    head(),
    body(
      component
    )
  ).toString()

writeFile(new URL("./index.html", import.meta.url), prerendered)

Adding Attributes

Nested elements are only half of the story, though. To be complete, we also need to be able to add attributes. To do so, we only need to place an object as the first argument, e.g. to make

<p id="my-paragraph">
  <span class="my-span">Some</span> text.
</p>

we can write

p({ id: "my-paragraph" },
  span({ class: "my-span" },
    "Some"
  ),
  " text."
)

Basic Composition

This notation for writing HTML in vanilla javascript becomes extremely powerful later on once you begin to functionally generate and compose components.

Here is a more realistic example of a static web page:

const info = {
  title: "My cool web page",
  description: "This web page is made with bruh"
}

const styles = ["./index.css", "/css/some-other.css"]
const scripts = ["./hydrate.mjs"]

html({ lang: "en-US" },
  head(
    // Title and description
    title(info.title),
    meta({
      name: "description",
      content: info.description
    }),

    // Useful metadata
    meta({ charset: "UTF-8" }),
    meta({
      name: "viewport",
      content: "width=device-width, initial-scale=1"
    }),

    // Include styles and scripts
    ...styles.map(href =>
      link({
        rel: "stylesheet",
        type: "text/css",
        href
      })
    ),
    ...scripts.map(src =>
      script({
        type: "module",
        src
      })
    )
  ),

  body(
    main(
      section({ id: "hero" },
        hgroup(
          h1({ class: "title" }, "Bruh."),
          h2({ class: "subtitle" },
            "The only library you need for your next web project"
          )
        )
      ),
      picture(
        img({
          class: "logo",
          alt: "The bruh logo",
          src: "/images/logo.svg",
          width: 256,
          height: 256
        })
      )
    )
  )
)

Dynamic DOM Interaction

That covers the simple function call API for building HTML, but there is more to HTML and the DOM than just nested elements with attributes. Each of the above elements is a MetaElement which is also a MetaNode. This is because the DOM also includes TextNodes, even though they aren't really a concept in HTML that you may write. TextNodes are useful for efficiently and cleanly manipulating substrings in the DOM, here is an example of how to work with them with bruh:

import { div, p, a } from "bruh/dom/html"
import { t } from "bruh/dom/meta-node"

const unixTime = t(Date.now())
  .setOnHydrate(node => {
    const updateTime = () => {
      node.textContent = Date.now()
      requestAnimationFrame(updateTime)
    }

    requestAnimationFrame(updateTime)
  })

const time =
  div(
    p("It has been ", unixTime, " milliseconds since the ",
      a({ href: "https://en.wikipedia.org/wiki/Unix_time" }, "Unix Epoch")
    )
  )

document.body.replaceChildren(time.toNode())

The way that the above works is:

  1. Makes a MetaTextNode with a default value: t(Date.now())

  2. Sets the onHydrate event handler for that text node (details below)

  3. Puts that unixTime MetaTextNode into a p MetaElement just like a normal string or any other MetaNode

  4. Adds it into the DOM, where it immediately runs

So what does the MetaNode.setOnHydate method do? Well, any MetaNode's are just objects that have methods to conveniently manipulate them inline, sort of like a "builder pattern".

MetaNode API

All MetaNode's have these properties and methods:

  • properties: an object corresponding to the DOM node's properties

  • addProperties({}): a method that uses Object.assign() to add new or overwrite old values in properties

  • toNode(): a method that creates a DOM node

  • toString(): a method that creates HTML

  • hydrateTag: a string that "tags" the node if it is made into an HTML string, so it can be selected during later hydration

  • setHydrateTag(""): a method to set the above string

  • hydrate(): a method that rebinds the MetaNode with a DOM node

  • onHydrate: a function that is called at the end of either toNode() or hydrate(). It is a callback that recieves a DOM node as its argument

  • setOnHydrate(node => {}): a method that sets the above function

Basically, the setOnHydrate method let us define a function that had access to the real DOM node right after it was created. This allowed for direct manipulation, conveniently right in the code that created the MetaNode.

MetaElement API

Note that there are a few more properties and methods for MetaElements specifically:

  • name: Elements have names, such as div, p, a, etc.

  • namespace: Usually unimportant unless making SVG elements. Only makes a difference when creating actual DOM nodes with toNode()

  • children: An array of MetaNode's and strings as children of the element

  • prepend(...children): A method that prepends to children

  • append(...children): A method that appends to children

  • attributes: An object corresponding to the element's attributes

  • addAttributes({}): Just like , addProperties() but for attributes

  • dataset: An object for attributes with the , data-whatever form

  • addDataAttributes({}): Just like addAttributes() but for dataset

Prerender + Hydration Example

So about hydration - how could we make the above example prerendered? Well, since bruh is so flexible due to its simplicity and scope, one possibility may look like this:

unix-time.mjs

import { t } from "bruh/dom/meta-node"

export default t(Date.now())
  .setOnHydrate(node => {
    const updateTime = () => {
      node.textContent = Date.now()
      requestAnimationFrame(updateTime)
    }

    requestAnimationFrame(updateTime)
  })
  .setHydrateTag("unix-time")

prerender.mjs

import unixTime from "./unix-time.mjs"

import {
  html, head, body, script,
  div, p, a
} from "bruh/dom/html"

import { writeFile } from "fs/promises"


const time =
  div(
    p("It has been ", unixTime, " milliseconds since the ",
      a({ href: "https://en.wikipedia.org/wiki/Unix_time" }, "Unix Epoch")
    )
  )

const prerendered =
  "<!doctype html>" +
  html(
    head(
      script({
        type: "module",
        src: "./hydrate.mjs"
      })
    ),
    body(
      time
    )
  ).toString()

writeFile(new URL("./index.html", import.meta.url), prerendered)

hydrate.mjs

import { unixTime } from "./unixTime.mjs"

unixTime.hydrate(document.querySelector(`[data-bruh-hydrate="unix-time"]`))

That's all that I have written down at the moment, but there is so much more. This covers the core of bruh, I hope it demonstrated how useful such a simple abstraction over the DOM can be. I have found that the DOM is extremely easy to work with, to the point of not needing a virtual DOM with diffing (like React) whatsoever. The flexibility of bruh makes it so that someone could easily implement a VDOM patch function, however.

I'm working on making a more complete rollup/(maybe vite) integration for bruh as well, but for now, you can see a bare-bones example here of how you can get started playing with bruh.

If you are adventurous, read the (relatively short) code here - I'm curious to hear your thoughts!