0.7.1 • Published 3 years ago

glowup v0.7.1

Weekly downloads
1
License
MIT
Repository
github
Last release
3 years ago

I'm using some of The Pandemic to make my own web tools, which includes this frontend module I'm calling glowup.

Please enjoy.

-C



glowup

glowup (npm) is a lightweight set of functions for building client applications that live close to the browser.

It's probably best defined by what it lacks:

  • No virtual DOM
  • No compiler or bundler requirements
  • No class based abstractions
  • No third party dependencies

It's MIT licensed. Have at it.

import { Tag, replace } from "https://unpkg.com/glowup@0.7.1/glowup.js"

replace(document.body, App())

function App() {
  let $button

  const onAttach = () => {
    console.log("This app was made with glowup!")
    console.log("https://github.com/connorspeers/glowup")
  }

  const onClick = () => {
    $button.disabled = true
    $button.textContent = "Redirecting you to the future..."
    setTimeout(() => window.location = "https://buildbackbetter.com", 2000)
  }

  return Body({
    className: "app",
    onAttach,
  },
    $button = Button({
      className: "app-button",
      textContent: "Tired of the last four years?",
      onClick,
    }),
  )
}

function Body(p, ...c)   { return Tag("body", p, ...c) }
function Button(p, ...c) { return Tag("button", p, ...c) }

index

Tag

Intro

Tag is HTML-in-JS for glownups. It creates an element with the given tag name, assigns the props, and appends the children. There's a handful of props that are handled differently from the others, but that's the gist.

/**
* @param {string} name - The tagName to provide to document.createElement
* @param {?Object} props - Properties to assign to the element after creation
* @param {...(Node|string|false)} children -  Falsy-filtered children to append
* @returns {Element} - The fully-composed Element
**/
export function Tag(name, props, ...children) {
  // ...
}

Its novelty comes from the pattern it unlocks when you use it like this:

// p -> props, c -> children
function Body(p, ...c)   { return Tag("body", p, ...c) }
function Button(p, ...c) { return Tag("button", p, ...c) }
// etc.

These one-liners can then be used in consuming components in ways that are structurally similar to HTML markup without the need for compilers.

function App() {
  return Body(null,
    Button({
      textContent: "Tired of the last four years?",
      onClick() {
        this.disabled = true
        this.textContent = "Redirecting you to the future..."
        setTimeout(() => window.location = "https://buildbackbetter.com", 2000)
      },
    }),
  )
}

You can capture element references in-place by using regular variables and assignment; no need for document.querySelector() or complex APIs. The following will work the same as the code above:

function App() {
  let $button

  const onClick = () => {
    $button.disabled = true
    $button.textContent = "Redirecting you to the future..."
    setTimeout(() => window.location = "https://buildbackbetter.com", 2000)
  }

  return Body(null,
    // Statements like this evaluate to the right hand side when used as values
    $button = Button({
      onClick,
      textContent: "Tired of the last four years?",
    }),
  )
}

There isn't any state-driven UI patterns by default, but it's not that hard to write your own.

function App() {
  let $app
  let count = 0

  const onClick = () => {
    count++
    $app.replaceWith(render())
  }

  const render = () => {
    return $app = Body(null,
      Button({
        onClick,
        textContent: `Clicked ${count} time${count === 1 ? '' : 's'}`,
      }),
    )
  }

  return render()
}

(Mind you, this is definitely a slow way to do state-driven UI. But so is React, even with the virtual DOM. 🤷‍♂️)

index

Props

The props are simply assigned to the created element in most cases, but there's some props that are non-standard and handled specially:

  • on[EventName]: As seen above, props starting with "on" with a third character equal to a capitalized letter are added to the element as event listeners. As of right now, they aren't checked to be functions first; they are added indiscriminately. To get the event name to listen for the "on" is removed and the rest of the property name is .toLowerCase()'d.
  • merge: Some element properties require complex merging when a callee component wants to forward property values from a caller. The merge prop is supposed to be an object of more props that will be applied after the current batch of props is finished being handled. Generally, merged props will override old values, but some (like event listeners) will compound instead of overriding. Example: Clicking the HelloWorld button below will cause "HELLO" and "WORLD" to be printed to the console, in that order.

    function HelloWorld() {
      return Hello({
        onClick() {
          console.log("WORLD")
        },
      })
    }
    
    function Hello(props) {
      return Button({
        merge: props,
        onClick() {
          console.log("HELLO")
        },
      })
    }

    Further, some merged props are special cases because overwriting previous values either wouldn't work or would be anti-pattern:

    • className: When merging this string, the new value is concatenated to the old value with a space separator
    • classList: This array of strings is falsy-filtered and added using the element.classList.add() method
    • style: This object merges with element.style, and includes CSS variable support
    • dataset: This object merges with element.dataset
    • ...Others? I'm sure there's more that require special merging logic but I haven't added them yet. It's on the todo list.

index

Children

The list of children can be DOM nodes, strings, or the value false and are appended to the element like usual after falsy-filtering. Falsy-filtering allows you to use short circuit rendering syntax if that's your thing. (Like how it works in React. But remember, re-rendering on every state update is fundamentally slow!)

index

dispatch

This function dispatches a non-bubbling, non-trickling event to the target.

For consistency with other event dispatchers, the original target is available on the detail object as .originalTarget.

/**
* @param {EventTarget} target - The element to dispatch the event to
* @param {string} typeArg - Passed into the CustomEvent constructor
* @param {Object} [detail={}] - Will be made available on the dispatched event
**/
export function dispatch(target, typeArg, detail={}) {
  // ...
}

"Why?" you ask...

// How it started
target.dispatchEvent(new CustomEvent("mycustomevent", { detail: { some: "data" } }))

// How it's going
dispatch(target, "mycustomevent", { some: "data" })

Yeah. Functions are great. Even the little ones.

index

bubble

This works the same as dispatch, but the dispatched event will bubble up through the tree.

/**
* @param {EventTarget} target - The target
* @param {string} typeArg - The type string passed into the CustomEvent constructor
* @param {Object} [detail={}] - Will be made available on the dispatched event
**/
function bubble(target, typeArg, detail={}) {
  // ...
}

index

trickle

This works the same as dispatch, but the dispatched event will trickle down to every node in the subtree rooted at the target. The parent node receives the event before the children, recursively.

Because this is a non-standard event distribution method, the event's .target will always be strictly equal to the event's .currentTarget. To get around this, there is an .originalTarget property available on the detail object which cannot be overridden.

/**
* @param {EventTarget} target - The target
* @param {string} typeArg - The type string passed into the CustomEvent constructor
* @param {Object} [detail={}] - Will be made available on the dispatched event
**/
function trickle(target, typeArg, detail={}) {
  // ...
}

Example: Clicking the button in the following App() will print "HELLO 2" and "WORLD 1" to the console, in that order. The count in the detail is included to demonstrate that the detail object is re-used for every dispatched element, not re-created or cloned.

function App() {
  let $app

  const log = (word, detail) => {
    console.log(`${word} ${detail.count}`)
    detail.count--
  }

  const click = () => {
    trickle($app, "helloworld", { count: 2 })
  }

  return $app = Body({
    onHelloWorld: event => log("HELLO", event.detail),
  },
    Button({
      textContent: "Click here and look at the console",
      onHelloWorld: event => log("WORLD", event.detail),
      onClick: click,
    }),
  )
}

The functions described in the next section all trigger the trickling events "attach" and "detach", which occur whenever an element is attached or detached from the document.body subtree.

index

append, replace, and remove

append will append a node with a list of children after falsy-filtering

/**
* @param {Node} node - The node to append to
* @param {...(Node|string|false)} children - Falsy-filtered nodes to append
**/
export function append(node, ...children) {
  // ...
}

replace will replace a node with a list of nodes after falsy-filtering

/**
* @param {Node} node - The node to replace
* @param {...(Node|string|false)} replacements - Falsy-filtered replacement nodes
**/
export function replace(node, ...replacements) {
  // ...
}

remove will remove a list of nodes from their parents (if any) after falsy-filtering

/**
* @param {...(Node|false)} nodes - Falsy-filtered nodes to remove
**/
export function remove(...nodes) {
  // ...
}

These functions trigger the "attach" and "detach" trickle events. An element receives the "attach" trickle event when it enters the document.body tree, and it receives the "detach" event when it leaves the document.body tree. An element that is already attached won't receive the attach event, and the same goes for detach. See the first example in this file to see the "attach" trickle event in action.

index

Usage

The usual ways:

  • unpkg + https - The following should work in 93% of browsers:

    // app.js
    import {
      Tag,
      append, replace, remove,
      dispatch, bubble, trickle,
    } from "https://unpkg.com/glowup"
    
    // Build app and attach it to document.body somehow here

    Example html file which uses this script:

    <!-- index.html -->
    <!DOCTYPE html><html lang="en"><head>
    
      <title>glowup: unpkg + https example</title>
      <meta charset="utf-8">
      <script type="module" src="/app.js"></script>
    
    </head><body>
    
      <noscript>i'm lazy and didn't replace this noscript tag, oopies</noscript>
    
    </body></html>

    Notice how there's no compile steps or shell commands to run?

  • npm + bundling - You can include this package as a dependency with:

    npm install glowup

    And then in your scripts:

    import {
      Tag,
      append, replace, remove,
      dispatch, bubble, trickle,
    } from "glowup"

    Finally, when building, you'd bundle it with the rest of your code using your favorite flavor of bundler. I liked rollup more than webpack, but you do you.

index


That's all folks! Thanks for reading.

Up next:

  • Finish finding the props which need special merging logic
  • - Testing (Deadline: v1)
  • Find and document drawbacks to this pattern
  • Explore other useful function ideas (render helpers?)
  • Examples are the best documentation
  • In-depth compare/contrast between this and popular web frameworks like React and Vue
0.7.1

3 years ago

0.7.0

3 years ago

0.6.3

3 years ago

0.6.4

3 years ago

0.6.2

3 years ago

0.6.1

3 years ago

0.6.0

3 years ago

0.5.1

3 years ago

0.5.0

3 years ago

0.4.0

3 years ago

0.3.0

4 years ago

0.2.1

4 years ago

0.2.3

4 years ago

0.2.2

4 years ago

0.2.4

4 years ago

0.2.0

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago