{"id":13998456,"url":"https://github.com/aidenybai/hundred","last_synced_at":"2025-04-05T03:09:31.410Z","repository":{"id":57269624,"uuid":"407235193","full_name":"aidenybai/hundred","owner":"aidenybai","description":"Build your own mini Million.js","archived":false,"fork":false,"pushed_at":"2023-06-13T08:46:34.000Z","size":84,"stargazers_count":443,"open_issues_count":0,"forks_count":17,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T02:05:12.502Z","etag":null,"topics":["block","block-virtual-dom","hundred","hundredjs","million","millionjs","react","tiny","vdom","virtual-dom"],"latest_commit_sha":null,"homepage":"https://million.dev/how","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aidenybai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":["aidenybai"]}},"created_at":"2021-09-16T16:21:55.000Z","updated_at":"2025-03-28T23:02:09.000Z","dependencies_parsed_at":"2024-01-15T19:58:21.554Z","dependency_job_id":null,"html_url":"https://github.com/aidenybai/hundred","commit_stats":{"total_commits":31,"total_committers":3,"mean_commits":"10.333333333333334","dds":0.06451612903225812,"last_synced_commit":"ac4ae3fbc35b9b386fb14db2679264cbe273bc2c"},"previous_names":["aidenybai/tiny-vdom"],"tags_count":3,"template":true,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aidenybai%2Fhundred","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aidenybai%2Fhundred/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aidenybai%2Fhundred/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aidenybai%2Fhundred/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aidenybai","download_url":"https://codeload.github.com/aidenybai/hundred/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247280272,"owners_count":20912967,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["block","block-virtual-dom","hundred","hundredjs","million","millionjs","react","tiny","vdom","virtual-dom"],"created_at":"2024-08-09T19:01:41.328Z","updated_at":"2025-04-05T03:09:31.391Z","avatar_url":"https://github.com/aidenybai.png","language":"TypeScript","funding_links":["https://github.com/sponsors/aidenybai"],"categories":["TypeScript"],"sub_categories":[],"readme":"# 💯 Hundred \u003cimg src=\"https://badgen.net/badgesize/brotli/https/unpkg.com/hundred?color=000000\u0026labelColor=00000\u0026label=bundle%20size\" alt=\"Code Size\" /\u003e \u003ca href=\"https://www.npmjs.com/package/hundred\" target=\"_blank\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/hundred?style=flat\u0026colorA=000000\u0026colorB=000000\" alt=\"NPM Version\" /\u003e\u003c/a\u003e\n\nHundred is intended to be a toy block virtual DOM based off of [Million.js](https://github.com/aidenybai/million), and is a proof-of-concept and a learning resource more than a tool you should actually use in production. This implementation is similarly based off of [`blockdom`](https://github.com/ged-odoo/blockdom).\n\n## How do I make a block virtual DOM?\n\nLet's go through a tutorial on how to create Hundred!\n\n**Recommended prerequisites**:\n\n- [Read \"How Million.js works\"](https://millionjs.org/docs#how-does-it-work)\n- [Add these types to your TypeScript file](https://github.com/aidenybai/hundred/blob/main/src/types.ts)\n\n### Step 1: Create a `h` function\n\nThe `h` function allows us to create virtual nodes. It takes in a tag name, an object of attributes, and an array of children. It returns a virtual DOM node.\n\n```typescript\n// Helper function to create virtual dom nodes\n// e.g. h('div', { id: 'foo' }, 'hello') =\u003e \u003cdiv id=\"foo\"\u003ehello\u003c/div\u003e\nexport const h = (\n  type: string,\n  props: Props = {},\n  ...children: VNode[]\n): VElement =\u003e ({\n  type,\n  props,\n  children,\n});\n\nconsole.log(h('div', { id: 'foo' }, 'hello'));\n// gives us { type: 'div', props: { id: 'foo' }, children: ['hello'] }\n\nconsole.log(h('div', { id: 'foo' }, h('span', null, 'hello')));\n// gives us { type: 'div', props: { id: 'foo' }, children: [{ type: 'span', props: null, children: ['hello'] }] }\n```\n\nEssentially, the virtual nodes are just plain JavaScript objects that represent the DOM nodes we want to create.\n\n### Step 2: Create a `block` function\n\nLet's assume that the user will provide some function `fn` that takes in some props and returns a virtual node. Basically, the props represent the data, and the virtual node represents the view (establishes a one-way data flow).\n\n```typescript\nexport const block = (fn: (props: Props) =\u003e VNode) =\u003e {\n  // ...\n};\n```\n\nOne thing about block virtual DOM is that we can create a \"mapping.\" Essentially, we need to figure out which props correspond to which virtual nodes. We can do this by passing a \"getter\" `Proxy` that will return a `Hole` (temporary placeholder for a future value) when we access a property.\n\n```typescript\n// Represents a property access on `props`\n// this.key is used to identify the property\n// Imagine an instance of Hole as a placeholder for a value\nclass Hole {\n  key: string;\n  constructor(key: string) {\n    this.key = key;\n  }\n}\n\nexport const block = (fn: (props: Props) =\u003e VNode) =\u003e {\n  // by using a proxy, we can intercept ANY property access on\n  // the object and return a Hole instance instead.\n  // e.g. props.any_prop =\u003e new Hole('any_prop')\n  const proxy = new Proxy(\n    {},\n    {\n      get(_, prop: string) {\n        return new Hole(prop);\n      },\n    }\n  );\n  // we pass the proxy to the function, so that it can\n  // replace property accesses with Hole placeholders\n  const vnode = fn(proxy);\n\n  // allows us to see instances of Hole inside the virtual node tree!\n  console.log(vnode);\n\n  // ...\n};\n```\n\n## Step 3: Implementing a `render` function\n\nOur barebones layout is effectively done, but now we need to implement static analysis to deal with those `Hole` placeholders. We can do this by creating a `render` function that takes in a virtual node and returns a real DOM node.\n\nLet's start by just creating the base function that turns virtual nodes into real DOM nodes:\n\n```typescript\n// Converts a virtual dom node into a real dom node.\n// It also tracks the edits that need to be made to the dom\nexport const render = (\n  // represents a virtual dom node, built w/ `h` function\n  vnode: VNode\n): HTMLElement | Text =\u003e {\n  if (typeof vnode === 'string') return document.createTextNode(vnode);\n\n  const el = document.createElement(vnode.type);\n\n  if (vnode.props) {\n    for (const name in vnode.props) {\n      const value = vnode.props[name];\n      el[name] = value;\n    }\n  }\n\n  for (let i = 0; i \u003c vnode.children?.length; i++) {\n    const child = vnode.children[i];\n    el.appendChild(render(child));\n  }\n\n  return el;\n};\n\nconsole.log(render(h('div', { id: 'foo' }, 'hello')));\n// gives us \u003cdiv id=\"foo\"\u003ehello\u003c/div\u003e\n```\n\nNow, we need to add the static analysis part. We can do this by adding two new parameters: `edits` and `path`. `edits` is an array of `Edit`, which represents our \"mapping.\" Each edit has data where the relevant DOM node is within the tree (via `path`), the key used to access `props` (via `hole`), the property name that we need to update (via `name`) if it is an attribute edit, and the index of the child (via `child`) if it is a child edit.\n\n```typescript\n// Converts a virtual dom node into a real dom node.\n// It also tracks the edits that need to be made to the dom\nexport const render = (\n  // represents a virtual dom node, built w/ `h` function\n  vnode: VNode,\n  // represents a list of edits to be made to the dom,\n  // processed by identifying `Hole` placeholder values\n  // in attributes and children.\n  //    NOTE: this is a mutable array, and we assume the user\n  //    passes in an empty array and uses that as a reference\n  //    for the edits.\n  edits: Edit[] = [],\n  // Path is used to keep track of where we are in the tree\n  // as we traverse it.\n  // e.g. [0, 1, 2] would mean:\n  //    el1 = 1st child of el\n  //    el2 = 2nd child of el1\n  //    el3 = 3rd child of el2\n  path: number[] = []\n): HTMLElement | Text =\u003e {\n  if (typeof vnode === 'string') return document.createTextNode(vnode);\n\n  const el = document.createElement(vnode.type);\n\n  if (vnode.props) {\n    for (const name in vnode.props) {\n      const value = vnode.props[name];\n      if (value instanceof Hole) {\n        edits.push({\n          type: 'attribute',\n          path, // the path we need to traverse to get to the element\n          attribute: name, // to set the value during mount/patch\n          hole: value.key, // to get the value from props during mount/patch\n        });\n        continue;\n      }\n      el[name] = value;\n    }\n  }\n\n  for (let i = 0; i \u003c vnode.children?.length; i++) {\n    const child = vnode.children[i];\n    if (child instanceof Hole) {\n      edits.push({\n        type: 'child',\n        path, // the path we need to traverse to get to the parent element\n        index: i, // index represents the position of the child in the parent used to insert/update the child during mount/patch\n        hole: child.key, // to get the value from props during mount/patch\n      });\n      continue;\n    }\n    // we respread the path to avoid mutating the original array\n    el.appendChild(render(child, edits, [...path, i]));\n  }\n\n  return el;\n};\n```\n\n## Step 4: Implementing a `mount` and `patch` function for blocks\n\nNow that we have a `render` function that can handle `Hole` placeholders, we can implement a `mount` function that takes in a virtual node and mounts it to the DOM. We can also implement a `patch` function that takes in a new virtual node and patches the DOM with the new changes.\n\nThere are some notable differences between `mount` and `patch`:\n\nWithin `mount`, we will create a copy of the DOM node that `render` produces. This is because we want to keep the original DOM node around so that we can use it to patch the DOM later. Also, we need to track element references for each Edit so that we can use them to patch the DOM later.\n\nWithin `patch`, we will use the original DOM node that we created in `mount` to patch the DOM. This is different because `mount` will insert or create new nodes, while `patch` will only update existing nodes.\n\n```typescript\n// block is a factory function that returns a function that\n// can be used to create a block. Imagine it as a live instance\n// you can use to patch it against instances of itself.\nexport const block = (fn: (props: Props) =\u003e VNode) =\u003e {\n  // by using a proxy, we can intercept ANY property access on\n  // the object and return a Hole instance instead.\n  // e.g. props.any_prop =\u003e new Hole('any_prop')\n  const proxy = new Proxy(\n    {},\n    {\n      get(_, prop: string) {\n        return new Hole(prop);\n      },\n    }\n  );\n  // we pass the proxy to the function, so that it can\n  // replace property accesses with Hole placeholders\n  const vnode = fn(proxy);\n\n  // edits is a mutable array, so we pass it by reference\n  const edits: Edit[] = [];\n  // by rendering the vnode, we also populate the edits array\n  // by parsing the vnode for Hole placeholders\n  const root = render(vnode, edits);\n\n  // factory function to create instances of this block\n  return (props: Props): Block =\u003e {\n    // elements stores the element references for each edit\n    // during mount, which can be used during patch later\n    const elements = new Array(edits.length);\n\n    // mount puts the element for the block on some parent element\n    const mount = (parent: HTMLElement) =\u003e {\n      // cloneNode saves memory by not reconstrcuting the dom tree\n      const el = root.cloneNode(true);\n      // we assume our rendering scope is just one block\n      parent.textContent = '';\n      parent.appendChild(el);\n\n      for (let i = 0; i \u003c edits.length; i++) {\n        const edit = edits[i];\n        // walk the tree to find the element / hole\n        let thisEl = el;\n        // If path = [1, 2, 3]\n        // thisEl = el.childNodes[1].childNodes[2].childNodes[3]\n        for (let i = 0; i \u003c edit.path.length; i++) {\n          thisEl = thisEl.childNodes[edit.path[i]];\n        }\n\n        // make sure we save the element reference\n        elements[i] = thisEl;\n\n        // this time, we can get the value from props\n        const value = props[edit.hole];\n\n        if (edit.type === 'attribute') {\n          thisEl[edit.attribute] = value;\n        } else if (edit.type === 'child') {\n          const textNode = document.createTextNode(value);\n          thisEl.insertBefore(textNode, thisEl.childNodes[edit.index]);\n        }\n      }\n    };\n\n    // patch updates the element references with new values\n    const patch = (newBlock: Block) =\u003e {\n      for (let i = 0; i \u003c edits.length; i++) {\n        const edit = edits[i];\n        const value = props[edit.hole];\n        const newValue = newBlock.props[edit.hole];\n\n        // dirty check\n        if (value === newValue) continue;\n\n        const thisEl = elements[i];\n\n        if (edit.type === 'attribute') {\n          thisEl[edit.attribute] = newValue;\n        } else if (edit.type === 'child') {\n          thisEl.childNodes[edit.index].textContent = newValue;\n        }\n      }\n    };\n\n    return { mount, patch, props, edits };\n  };\n};\n```\n\nThis is great, but it's not really a virtual DOM–it only allows us to create one block and patch it against itself. Oftentimes, we want to construct these blocks into trees.\n\nSo, let's add a special case for block values in props.\n\n```typescript\n// block is a factory function that returns a function that\n// can be used to create a block. Imagine it as a live instance\n// you can use to patch it against instances of itself.\nexport const block = (fn: (props: Props) =\u003e VNode) =\u003e {\n  // by using a proxy, we can intercept ANY property access on\n  // the object and return a Hole instance instead.\n  // e.g. props.any_prop =\u003e new Hole('any_prop')\n  const proxy = new Proxy(\n    {},\n    {\n      get(_, prop: string) {\n        return new Hole(prop);\n      },\n    }\n  );\n  // we pass the proxy to the function, so that it can\n  // replace property accesses with Hole placeholders\n  const vnode = fn(proxy);\n\n  // edits is a mutable array, so we pass it by reference\n  const edits: Edit[] = [];\n  // by rendering the vnode, we also populate the edits array\n  // by parsing the vnode for Hole placeholders\n  const root = render(vnode, edits);\n\n  // factory function to create instances of this block\n  return (props: Props): Block =\u003e {\n    // elements stores the element references for each edit\n    // during mount, which can be used during patch later\n    const elements = new Array(edits.length);\n\n    // mount puts the element for the block on some parent element\n    const mount = (parent: HTMLElement) =\u003e {\n      // cloneNode saves memory by not reconstrcuting the dom tree\n      const el = root.cloneNode(true);\n      // we assume our rendering scope is just one block\n      parent.textContent = '';\n      parent.appendChild(el);\n\n      for (let i = 0; i \u003c edits.length; i++) {\n        const edit = edits[i];\n        // walk the tree to find the element / hole\n        let thisEl = el;\n        // If path = [1, 2, 3]\n        // thisEl = el.childNodes[1].childNodes[2].childNodes[3]\n        for (let i = 0; i \u003c edit.path.length; i++) {\n          thisEl = thisEl.childNodes[edit.path[i]];\n        }\n\n        // make sure we save the element reference\n        elements[i] = thisEl;\n\n        // this time, we can get the value from props\n        const value = props[edit.hole];\n\n        if (edit.type === 'attribute') {\n          thisEl[edit.attribute] = value;\n        } else if (edit.type === 'child') {\n          // handle nested blocks if the value is a block\n          if (value.mount \u0026\u0026 typeof value.mount === 'function') {\n            value.mount(thisEl);\n            continue;\n          }\n\n          const textNode = document.createTextNode(value);\n          thisEl.insertBefore(textNode, thisEl.childNodes[edit.index]);\n        }\n      }\n    };\n\n    // patch updates the element references with new values\n    const patch = (newBlock: Block) =\u003e {\n      for (let i = 0; i \u003c edits.length; i++) {\n        const edit = edits[i];\n        const value = props[edit.hole];\n        const newValue = newBlock.props[edit.hole];\n\n        // dirty check\n        if (value === newValue) continue;\n\n        const thisEl = elements[i];\n\n        if (edit.type === 'attribute') {\n          thisEl[edit.attribute] = newValue;\n        } else if (edit.type === 'child') {\n          // handle nested blocks if the value is a block\n          if (value.patch \u0026\u0026 typeof value.patch === 'function') {\n            // patch cooresponding child blocks\n            value.patch(newBlock.edits[i].hole);\n            continue;\n          }\n          thisEl.childNodes[edit.index].textContent = newValue;\n        }\n      }\n    };\n\n    return { mount, patch, props, edits };\n  };\n};\n```\n\nIf you want to view the full source code, check out [src/index.ts](https://github.com/aidenybai/hundred/blob/main/src/index.ts).\n\n## Install Hundred\n\nInside your project directory, run the following command:\n\n```sh\nnpm install hundred\n```\n\n## Usage\n\n```js\nimport { h, block } from 'hundred';\n\nconst Button = block(({ number }) =\u003e {\n  return h('button', null, number);\n});\n\nconst button = Button({ number: 0 });\n\nbutton.mount(document.getElementById('root'));\n\nsetInterval(() =\u003e {\n  button.patch(Button({ number: Math.random() }));\n}, 100);\n```\n\n## License\n\n`hundred` is [MIT-licensed](LICENSE) open-source software by [Aiden Bai](https://github.com/aidenybai).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faidenybai%2Fhundred","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faidenybai%2Fhundred","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faidenybai%2Fhundred/lists"}