Skip to content

Develop lightweight and declarative UI with automatic dependecy tracking without boilerplate code, VDOM, nor compiler

License

Notifications You must be signed in to change notification settings

beenotung/dom-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dom-proxy

Develop lightweight and declarative UI with automatic dependecy tracking without boilerplate code, VDOM, nor compiler.

npm Package Version Minified Package Size Minified and Gzipped Package Size

Demo: https://dom-proxy.surge.sh

Quick Example

import { watch, input, text, fragment, label, p } from 'dom-proxy'

let nameInput = input({ placeholder: 'guest', id: 'visitor-name' })
let nameText = text()

// auto re-run when the value in changed
watch(() => {
  nameText.textContent = nameInput.value || nameInput.placeholder
})

document.body.appendChild(
  fragment([
    label({ textContent: 'name: ', htmlFor: nameInput.id }),
    nameInput,
    p({}, ['hello, ', nameText]),
  ]),
)

Complete example see quick-example.ts

(Explained in the usage examples section)

Installation

You can get dom-proxy via npm:

npm install dom-proxy

Then import from typescript using named import or star import:

import { watch } from 'dom-proxy'
import * as domProxy from 'dom-proxy'

Or import from javascript as commonjs module:

var domProxy = require('dom-proxy')

You can also get dom-proxy directly in html via CDN:

<script src="https://cdn.jsdelivr.net/npm/dom-proxy@1/browser.min.js"></script>
<script>
  console.log(typeof domProxy.watch) // function
</script>

Usage Examples

More examples can be found in ./demo:

Example using creation functions

This example consists of a input and text message.

With the watch() function, the text message is initialied and updated according to the input value. We don't need to specify the dependency explicitly.

import { watch, input, span, label, fragment } from 'dom-proxy'

let nameInput = input({ placeholder: 'guest', id: 'visitor-name' })
let nameSpan = span()

// the read-dependencies are tracked automatically
watch(() => {
  nameSpan.textContent = nameInput.value || nameInput.placeholder
})

document.body.appendChild(
  // use a DocumentFragment to contain the elements
  fragment([
    label({ textContent: 'name: ', htmlFor: nameInput.id }),
    nameInput,
    p({}, ['hello, ', nameSpan]),
  ]),
)

Example using selector functions

This example query and proxy the existing elements from the DOM, then setup interactive logics in the watch() function.

If the selectors don't match any element, it will throw error.

import { ProxyNode, watch } from 'dom-proxy'
import { queryElement, queryElementProxies } from 'dom-proxy'

let loginForm = queryElement<HTMLFormElement>('#loginForm')
let elements = queryElementProxies(
  {
    username: '[name=username]',
    password: '[name=password]',
    preview: '#preview',
    reset: '[type=reset]',
    submit: '[type=submit]',
  },
  loginForm,
)
const preview = elements.preview
const username = elements.username as ProxyNode<HTMLInputElement>
const password = elements.password as ProxyNode<HTMLInputElement>
const reset = elements.reset as ProxyNode<HTMLInputElement>
const submit = elements.submit as ProxyNode<HTMLInputElement>

watch(() => {
  preview.textContent = username.value + ':' + password.value
})

watch(() => {
  reset.disabled = !username.value && !password.value
  submit.disabled = !username.value || !password.value
})

Typescript Signature

The types shown in this section are simplified, see the .d.ts files published in the npm package for complete types.

Reactive function

/** run once immediately, auto track dependency and re-run */
function watch(fn: Function): void

Selector functions

These query selector functions will throw error if no elements match the selectors.

function queryElement<Element>(selector: string, parent?: ParentNode): Element

function queryElementProxy<Element>(
  selector: string,
  parent?: ParentNode,
): ProxyNode<Element>

function queryElements<K, Element>(
  selectors: Record<K, string>,
  parent?: ParentNode,
): Record<K, Element>

function queryElementProxies<K, Element>(
  selectors: Record<K, string>,
  parent?: ParentNode,
): Record<K, ProxyNode<Element>>

Creation functions

function fragment(nodes: NodeChild[]): DocumentFragment

/** @alias t, text */
function createText(value?: string | number): ProxyNode<Text>

/** @alias h, html */
function createHTMLElement<K, Element>(
  tagName: K,
  props?: Properties<Element> & CreateProxyOptions,
  children?: NodeChild[],
): ProxyNode<Element>

/** @alias s, svg */
function createSVGElement<K, SVGElement>(
  tagName: K,
  props?: Properties<SVGElement> & CreateProxyOptions,
  children?: NodeChild[],
): ProxyNode<SVGElement>

function createProxy<Node>(
  node: Node,
  options?: CreateProxyOptions,
): ProxyNode<Node>

Creation helper functions

The creation function of most html elements and svg elements are defined as partially applied createHTMLElement() or createSVGElement().

If you need more helper functions (e.g. for custom web components or deprecated elements[1]), you can defined them with genCreateHTMLElement(tagName) or genCreateSVGElement(tagName)

The type of creation functions are inferred from the tag name with HTMLElementTagNameMap and SVGElementTagNameMap.

Below are some example types:

// some pre-defined creation helper functions
const div: PartialCreateElement<HTMLDivElement>,
  p: PartialCreateElement<HTMLParagraphElement>,
  a: PartialCreateElement<HTMLAnchorElement>,
  label: PartialCreateElement<HTMLLabelElement>,
  input: PartialCreateElement<HTMLInputElement>,
  path: PartialCreateElement<SVGPathElement>,
  polyline: PartialCreateElement<SVGPolylineElement>,
  rect: PartialCreateElement<SVGRectElement>
// and more ...

For most elements, the creation functions use the same name as the tag name, however some are renamed to avoid name clash.

Renamed html element creation functions:

  • html -> htmlElement
  • s -> sElement
  • script -> scriptElement
  • style -> styleElement
  • title -> titleElement
  • var -> varElement

Renamed svg elements creation functions:

  • a -> aSVG
  • script -> scriptSVG
  • style -> styleSVG
  • svg -> svgSVG
  • switch -> switchSVG
  • text -> textSVG
  • title -> titleSVG
Tips to rename the creation functions (click to expand)

The creation functions are defined dynamically in the proxy object createHTMLElementFunctions and createSVGElementFunctions

If you prefer to rename them with different naming conventions, you can destruct from the proxy object using your preferred name. For example:

const { s, style, var_ } = createHTMLElementFunctions
const { a, text } = createSVGElementFunctions
const {
  html: { a: html_a, style: htmlStyle },
  svg: { a: svg_a, style: svgStyle },
} = createElementFunctions

You can also use them without renaming, e.g.:

const h = createHTMLElementFunctions

let style = document.body.appendChild(
  fragment([
    // you can use the creation functions without extracting into top-level const
    h.s({ textContent: 'Now on sales' }),
    'Sold out',
  ]),
)

The types of the proxies are listed below:

type CreateHTMLElementFunctions = {
  [K in keyof HTMLElementTagNameMap]: PartialCreateElement<
    HTMLElementTagNameMap[K]
  >
}
const createHTMLElementFunctions: CreateHTMLElementFunctions

type CreateSVGElementFunctions = {
  [K in keyof SVGElementTagNameMap]: PartialCreateElement<
    SVGElementTagNameMap[K]
  >
}
const createSVGElementFunctions: CreateSVGElementFunctions

const createElementFunctions: {
  html: CreateHTMLElementFunctions
  svg: CreateSVGElementFunctions
}

[1]: Some elements are deprecated in html5, e.g. dir, font, frame, frameset, marquee, param. They are not predefined to avoid tsc error in case their type definition are not included.

Partially applied creation functions

These are some high-order functions that helps to generate type-safe creation functions for specific elements with statically typed properties.

/** partially applied createHTMLElement */
function genCreateHTMLElement<K extends keyof HTMLElementTagNameMap>(
  tagName: K,
): PartialCreateElement<HTMLElementTagNameMap[K]>

/** partially applied createSVGElement */
function genCreateSVGElement<K extends keyof SVGElementTagNameMap>(
  tagName: K,
): PartialCreateElement<SVGElementTagNameMap[K]>

Options Types / Output Types

type ProxyNode<E> = E & {
  node: E
}

type NodeChild = Node | ProxyNode | string | number

type CreateProxyOptions = {
  listen?: 'change' | 'input' | false
}

type Properties<E> = Partial<{
  [P in keyof E]?: E[P] extends object ? Partial<E[P]> : E[P]
}>

type PartialCreateElement<Element> = (
  props?: Properties<Element> & CreateProxyOptions,
  children?: NodeChild[],
) => ProxyNode<Element>

License

This project is licensed with BSD-2-Clause

This is free, libre, and open-source software. It comes down to four essential freedoms [ref]:

  • The freedom to run the program as you wish, for any purpose
  • The freedom to study how the program works, and change it so it does your computing as you wish
  • The freedom to redistribute copies so you can help others
  • The freedom to distribute copies of your modified versions to others

About

Develop lightweight and declarative UI with automatic dependecy tracking without boilerplate code, VDOM, nor compiler

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published