npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

dom-proxy

v2.2.1

Published

Develop declarative UI with (opt-in) automatic dependency tracking without boilerplate code, VDOM, nor compiler.

Downloads

19

Readme

dom-proxy

Develop lightweight and declarative UI with automatic dependency tracking in Javascript/Typescript without boilerplate code, VDOM, nor compiler.

npm Package Version Minified Package Size Minified and Gzipped Package Size

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

Table of Content

Quick Example

// elements type are inferred from selector
let { password, showPw } = queryElementProxies({
  showPw: 'input#show-pw',
  password: '[name=password]',
})

watch(() => {
  password.type = showPw.checked ? 'text' : 'password'
})

// create new element or text node, then proxy on it
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@2/browser.min.js"></script>
<script>
  console.log(typeof domProxy.watch) // function
</script>

How it works

A DOM proxy can be used to enable reactive programming by intercepting access to a DOM node's properties and triggering updates to the UI whenever those properties are changed.

Here's an example of how a DOM proxy can be used to enable reactive programming:

const nameInput = document.querySelector('input#name')
const message = document.querySelector('p#message')

const inputProxy = new Proxy(nameInput, {
  set(target, property, value) {
    target[property] = value
    message.textContent = 'Hello, ' + value + '!'
    return true
  },
})

inputProxy.value = 'world'

In this example, we've created a reactive input element by creating a DOM proxy for the input element. The set trap of the proxy is used to intercept any changes made to the input's value, and it updates the output element's text content to reflect the new value.

However, it is quite verbose to work with the Proxy API directly.

dom-proxy allows you to do reactive programming concisely. With dom-proxy, above example can be written as:

let { nameInput, message } = queryElementProxies({
  nameInput: 'input#name',
  message: 'p#message',
})

watch(() => {
  message.textContent = 'Hello, ' + nameInput.value + '!'
})

nameInput.value = 'world'

In above example, the textContent of message depends on the value of nameInput, this dependency is automatically tracked without explicitly coding.

This is in contrast to useEffect() in React where you have to manually maintain the dependency list. Also, dom-proxy works in mutable manner, hence we don't need to run "diffing" algorithm on VDOM to reconciliate the UI.

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 initialized 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('form#loginForm') // infer to be HTMLFormElement
let { password, showPw } = queryElementProxies(
  {
    showPw: 'input#show-pw', // infer to be ProxyNode<HTMLInputElement> <- "input" tagName
    password: '[name=password]', // fallback to be ProxyNode<HTMLInputElement> <- "[name=.*]" attribute without tagName
  },
  loginForm,
)

watch(() => {
  password.type = showPw.checked ? 'text' : 'password'
})

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

/** @description run once immediately, auto track dependency and re-run */
function watch(
  fn: Function,
  options?: {
    listen?: 'change' | 'input' // default 'input'
  },
): void

Selector functions

These query selector functions (except queryAll*()) will throw error if no elements match the selectors.

The corresponding element type is inferred from the tag name in the selector. (e.g. select[name=theme] will be inferred as HTMLSelectElement)

If the selector doesn't contain the tag name but containing "name" attribute (e.g. [name=password]), the inferred type will be HTMLInputElement.

If the element type cannot be determined, it will fallback to Element type.

function queryElement<Selector extends string>(
  selector: Selector,
  parent?: ParentNode,
): InferElement<Selector>

function queryElementProxy<Selector extends string>(
  selector: Selector,
  parent?: ParentNode,
): ProxyNode<InferElement<Selector>>

function queryAllElements<Selector extends string>(
  selector: Selector,
  parent?: ParentNode,
): InferElement<Selector>[]

function queryAllElementProxies<Selector extends string>(
  selector: Selector,
  parent?: ParentNode,
): ProxyNode<InferElement<Selector>>[]

function queryElements<SelectorDict extends Dict<string>>(
  selectors: SelectorDict,
  parent?: ParentNode,
): { [P in keyof SelectorDict]: InferElement<SelectorDict[P]> }

function queryElementProxies<SelectorDict extends Dict<string>>(
  selectors: SelectorDict,
  parent?: ParentNode,
): { [P in keyof SelectorDict]: ProxyNode<InferElement<SelectorDict[P]>> }

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>,
  children?: NodeChild[],
): ProxyNode<Element>

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

function createProxy<Node>(node: Node): 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

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:

// you can destruct into custom alias from `createHTMLElementFunctions`
const { s, style, var: var_ } = createHTMLElementFunctions
// or destruct from `createSVGElementFunctions`
const { a, text } = createSVGElementFunctions
// or destruct from createElementFunctions, which wraps above two objects as `html` and `svg`
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 Properties<E> = Partial<{
  [P in keyof E]?: E[P] extends object ? Partial<E[P]> : E[P]
}>

interface PartialCreateElement<Element> {
  (props?: Properties<Element>, children?: NodeChild[]): ProxyNode<Element>
  (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