• Stars
    star
    425
  • Rank 98,210 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 2 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Build your own mini Million.js

๐Ÿ’ฏ Hundred Code Size NPM Version

Hundred is intended to be a toy block virtual DOM based off of Million.js, 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.

How do I make a block virtual DOM?

Let's go through a tutorial on how to create Hundred!

Recommended prerequisites:

Step 1: Create a h function

The 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.

// Helper function to create virtual dom nodes
// e.g. h('div', { id: 'foo' }, 'hello') => <div id="foo">hello</div>
export const h = (
  type: string,
  props: Props = {},
  ...children: VNode[]
): VElement => ({
  type,
  props,
  children,
});

console.log(h('div', { id: 'foo' }, 'hello'));
// gives us { type: 'div', props: { id: 'foo' }, children: ['hello'] }

console.log(h('div', { id: 'foo' }, h('span', null, 'hello')));
// gives us { type: 'div', props: { id: 'foo' }, children: [{ type: 'span', props: null, children: ['hello'] }] }

Essentially, the virtual nodes are just plain JavaScript objects that represent the DOM nodes we want to create.

Step 2: Create a block function

Let'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).

export const block = (fn: (props: Props) => VNode) => {
  // ...
};

One 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.

// Represents a property access on `props`
// this.key is used to identify the property
// Imagine an instance of Hole as a placeholder for a value
class Hole {
  key: string;
  constructor(key: string) {
    this.key = key;
  }
}

export const block = (fn: (props: Props) => VNode) => {
  // by using a proxy, we can intercept ANY property access on
  // the object and return a Hole instance instead.
  // e.g. props.any_prop => new Hole('any_prop')
  const proxy = new Proxy(
    {},
    {
      get(_, prop: string) {
        return new Hole(prop);
      },
    }
  );
  // we pass the proxy to the function, so that it can
  // replace property accesses with Hole placeholders
  const vnode = fn(proxy);

  // allows us to see instances of Hole inside the virtual node tree!
  console.log(vnode);

  // ...
};

Step 3: Implementing a render function

Our 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.

Let's start by just creating the base function that turns virtual nodes into real DOM nodes:

// Converts a virtual dom node into a real dom node.
// It also tracks the edits that need to be made to the dom
export const render = (
  // represents a virtual dom node, built w/ `h` function
  vnode: VNode
): HTMLElement | Text => {
  if (typeof vnode === 'string') return document.createTextNode(vnode);

  const el = document.createElement(vnode.type);

  if (vnode.props) {
    for (const name in vnode.props) {
      const value = vnode.props[name];
      el[name] = value;
    }
  }

  for (let i = 0; i < vnode.children?.length; i++) {
    const child = vnode.children[i];
    el.appendChild(render(child));
  }

  return el;
};

console.log(render(h('div', { id: 'foo' }, 'hello')));
// gives us <div id="foo">hello</div>

Now, 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.

// Converts a virtual dom node into a real dom node.
// It also tracks the edits that need to be made to the dom
export const render = (
  // represents a virtual dom node, built w/ `h` function
  vnode: VNode,
  // represents a list of edits to be made to the dom,
  // processed by identifying `Hole` placeholder values
  // in attributes and children.
  //    NOTE: this is a mutable array, and we assume the user
  //    passes in an empty array and uses that as a reference
  //    for the edits.
  edits: Edit[] = [],
  // Path is used to keep track of where we are in the tree
  // as we traverse it.
  // e.g. [0, 1, 2] would mean:
  //    el1 = 1st child of el
  //    el2 = 2nd child of el1
  //    el3 = 3rd child of el2
  path: number[] = []
): HTMLElement | Text => {
  if (typeof vnode === 'string') return document.createTextNode(vnode);

  const el = document.createElement(vnode.type);

  if (vnode.props) {
    for (const name in vnode.props) {
      const value = vnode.props[name];
      if (value instanceof Hole) {
        edits.push({
          type: 'attribute',
          path, // the path we need to traverse to get to the element
          attribute: name, // to set the value during mount/patch
          hole: value.key, // to get the value from props during mount/patch
        });
        continue;
      }
      el[name] = value;
    }
  }

  for (let i = 0; i < vnode.children?.length; i++) {
    const child = vnode.children[i];
    if (child instanceof Hole) {
      edits.push({
        type: 'child',
        path, // the path we need to traverse to get to the parent element
        index: i, // index represents the position of the child in the parent used to insert/update the child during mount/patch
        hole: child.key, // to get the value from props during mount/patch
      });
      continue;
    }
    // we respread the path to avoid mutating the original array
    el.appendChild(render(child, edits, [...path, i]));
  }

  return el;
};

Step 4: Implementing a mount and patch function for blocks

Now 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.

There are some notable differences between mount and patch:

Within 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.

Within 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.

// block is a factory function that returns a function that
// can be used to create a block. Imagine it as a live instance
// you can use to patch it against instances of itself.
export const block = (fn: (props: Props) => VNode) => {
  // by using a proxy, we can intercept ANY property access on
  // the object and return a Hole instance instead.
  // e.g. props.any_prop => new Hole('any_prop')
  const proxy = new Proxy(
    {},
    {
      get(_, prop: string) {
        return new Hole(prop);
      },
    }
  );
  // we pass the proxy to the function, so that it can
  // replace property accesses with Hole placeholders
  const vnode = fn(proxy);

  // edits is a mutable array, so we pass it by reference
  const edits: Edit[] = [];
  // by rendering the vnode, we also populate the edits array
  // by parsing the vnode for Hole placeholders
  const root = render(vnode, edits);

  // factory function to create instances of this block
  return (props: Props): Block => {
    // elements stores the element references for each edit
    // during mount, which can be used during patch later
    const elements = new Array(edits.length);

    // mount puts the element for the block on some parent element
    const mount = (parent: HTMLElement) => {
      // cloneNode saves memory by not reconstrcuting the dom tree
      const el = root.cloneNode(true);
      // we assume our rendering scope is just one block
      parent.textContent = '';
      parent.appendChild(el);

      for (let i = 0; i < edits.length; i++) {
        const edit = edits[i];
        // walk the tree to find the element / hole
        let thisEl = el;
        // If path = [1, 2, 3]
        // thisEl = el.childNodes[1].childNodes[2].childNodes[3]
        for (let i = 0; i < edit.path.length; i++) {
          thisEl = thisEl.childNodes[edit.path[i]];
        }

        // make sure we save the element reference
        elements[i] = thisEl;

        // this time, we can get the value from props
        const value = props[edit.hole];

        if (edit.type === 'attribute') {
          thisEl[edit.attribute] = value;
        } else if (edit.type === 'child') {
          const textNode = document.createTextNode(value);
          thisEl.insertBefore(textNode, thisEl.childNodes[edit.index]);
        }
      }
    };

    // patch updates the element references with new values
    const patch = (newBlock: Block) => {
      for (let i = 0; i < edits.length; i++) {
        const edit = edits[i];
        const value = props[edit.hole];
        const newValue = newBlock.props[edit.hole];

        // dirty check
        if (value === newValue) continue;

        const thisEl = elements[i];

        if (edit.type === 'attribute') {
          thisEl[edit.attribute] = newValue;
        } else if (edit.type === 'child') {
          thisEl.childNodes[edit.index].textContent = newValue;
        }
      }
    };

    return { mount, patch, props, edits };
  };
};

This 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.

So, let's add a special case for block values in props.

// block is a factory function that returns a function that
// can be used to create a block. Imagine it as a live instance
// you can use to patch it against instances of itself.
export const block = (fn: (props: Props) => VNode) => {
  // by using a proxy, we can intercept ANY property access on
  // the object and return a Hole instance instead.
  // e.g. props.any_prop => new Hole('any_prop')
  const proxy = new Proxy(
    {},
    {
      get(_, prop: string) {
        return new Hole(prop);
      },
    }
  );
  // we pass the proxy to the function, so that it can
  // replace property accesses with Hole placeholders
  const vnode = fn(proxy);

  // edits is a mutable array, so we pass it by reference
  const edits: Edit[] = [];
  // by rendering the vnode, we also populate the edits array
  // by parsing the vnode for Hole placeholders
  const root = render(vnode, edits);

  // factory function to create instances of this block
  return (props: Props): Block => {
    // elements stores the element references for each edit
    // during mount, which can be used during patch later
    const elements = new Array(edits.length);

    // mount puts the element for the block on some parent element
    const mount = (parent: HTMLElement) => {
      // cloneNode saves memory by not reconstrcuting the dom tree
      const el = root.cloneNode(true);
      // we assume our rendering scope is just one block
      parent.textContent = '';
      parent.appendChild(el);

      for (let i = 0; i < edits.length; i++) {
        const edit = edits[i];
        // walk the tree to find the element / hole
        let thisEl = el;
        // If path = [1, 2, 3]
        // thisEl = el.childNodes[1].childNodes[2].childNodes[3]
        for (let i = 0; i < edit.path.length; i++) {
          thisEl = thisEl.childNodes[edit.path[i]];
        }

        // make sure we save the element reference
        elements[i] = thisEl;

        // this time, we can get the value from props
        const value = props[edit.hole];

        if (edit.type === 'attribute') {
          thisEl[edit.attribute] = value;
        } else if (edit.type === 'child') {
          // handle nested blocks if the value is a block
          if (value.mount && typeof value.mount === 'function') {
            value.mount(thisEl);
            continue;
          }

          const textNode = document.createTextNode(value);
          thisEl.insertBefore(textNode, thisEl.childNodes[edit.index]);
        }
      }
    };

    // patch updates the element references with new values
    const patch = (newBlock: Block) => {
      for (let i = 0; i < edits.length; i++) {
        const edit = edits[i];
        const value = props[edit.hole];
        const newValue = newBlock.props[edit.hole];

        // dirty check
        if (value === newValue) continue;

        const thisEl = elements[i];

        if (edit.type === 'attribute') {
          thisEl[edit.attribute] = newValue;
        } else if (edit.type === 'child') {
          // handle nested blocks if the value is a block
          if (value.patch && typeof value.patch === 'function') {
            // patch cooresponding child blocks
            value.patch(newBlock.edits[i].hole);
            continue;
          }
          thisEl.childNodes[edit.index].textContent = newValue;
        }
      }
    };

    return { mount, patch, props, edits };
  };
};

If you want to view the full source code, check out src/index.ts.

Install Hundred

Inside your project directory, run the following command:

npm install hundred

Usage

import { h, block } from 'hundred';

const Button = block(({ number }) => {
  return h('button', null, number);
});

const button = Button({ number: 0 });

button.mount(document.getElementById('root'));

setInterval(() => {
  button.patch(Button({ number: Math.random() }));
}, 100);

License

hundred is MIT-licensed open-source software by Aiden Bai.

More Repositories

1

pattycake

Zero-runtime pattern matching
TypeScript
760
star
2

lucia

๐Ÿ™‹โ€โ™€๏ธ 3kb library for tiny web apps
TypeScript
739
star
3

million-react

โš›๏ธ Vite starter for Million.js
CSS
424
star
4

reaict

Optimize React with AI
TypeScript
134
star
5

hacky

โš™๏ธ Crank.js with tagged templates
TypeScript
44
star
6

grumpy

๐Ÿ”‘ Painless key-value storage (deprecated)
TypeScript
36
star
7

snip

โœŒ๏ธ The simple, no-bs link shortener
TypeScript
35
star
8

dababy

:trollface: Data binding so simple even DaBaby could do it!
TypeScript
29
star
9

million-demo

Million vs. React demo
JavaScript
19
star
10

vhs

old school website vibes
JavaScript
18
star
11

million-site

๐Ÿฆ Million.js site built with Nextra
JavaScript
15
star
12

site

โŒจ๏ธ Personal website
JavaScript
12
star
13

site-mini

Minimal revamp of my personal site
CSS
12
star
14

vite-plugin-million

โšก Million.js' compiler powered by Vite.js
TypeScript
11
star
15

million-tanstack-virtual

JavaScript
11
star
16

babel-preset-million

โค๏ธโ€๐Ÿ”ฅ Transforms JSX to Million virtual nodes
JavaScript
10
star
17

mini

Extremely minimalistic website starter
TypeScript
9
star
18

docscan

๐Ÿ‘“ Scans documents and returns strings
Python
8
star
19

is-monday

โฐ Checks if today is Monday
JavaScript
8
star
20

kbowl-client

๐Ÿง  Conduct oral knowledge bowl online!
TypeScript
8
star
21

website

๐ŸŒ Source code of personal website.
HTML
8
star
22

aidenybai

:shipit: My GitHub readme
Shell
8
star
23

million-dev-setup

Million dev setup
TypeScript
8
star
24

lucia-starter

๐Ÿ“ฆ The official bundler boilerplate for Lucia
JavaScript
7
star
25

build-your-own-virtual-dom

BYO slides for presenting at conferences
Vue
7
star
26

revenge.ev3

๐Ÿ’ช Skyridge's Sumo bot program in ev3.
7
star
27

skywarder

๐Ÿ“˜ A simple and fast RESTful API for interacting with the Skyward grading system.
JavaScript
7
star
28

schzoom

Zoom scheduler dashboard for students written using Remake
JavaScript
7
star
29

tba

๐Ÿ’™ Wrapper for bluealliance.com
JavaScript
7
star
30

alastor

๐Ÿ˜ˆ๐Ÿค˜ Hellish-fast asynchronous HTTP client for NodeJS
TypeScript
7
star
31

www

โŒจ๏ธ Personal website
TypeScript
6
star
32

i.zephyr

The static-file hosting service that'll work like a breeze.
TypeScript
6
star
33

hastebin

โšก Module for creating hastebins
TypeScript
6
star
34

weebify

๐Ÿคก Frictionless text weebifier (AP CSA project w/@sarahberah)
JavaScript
6
star
35

amogus.church

sus
HTML
6
star
36

base

๐ŸŽจ My base javascript project template.
JavaScript
6
star
37

camas

Camas Codes website
CSS
5
star
38

virtual-dom-workshop

JavaScript
5
star
39

me

Minimalistic personal webpage
EJS
5
star
40

kbowl-server

Websocket IO for kbowl
JavaScript
5
star
41

million-compiler

JSX compiler for Million (proof of concept)
JavaScript
5
star
42

fuck.zephyr

take selfies by throwing up your middle finger
JavaScript
5
star
43

magnet-project

๐Ÿงฒ CHS Magnet Project API
JavaScript
5
star
44

aidenybai.github.io-old

๐Ÿ•ธ๏ธPersonal website
JavaScript
5
star
45

million-delta-react-test

dom vs. million-react vs. million-react delta
JavaScript
5
star
46

kbowl

๐Ÿง  App for oral knowledge bowl during COVID times
JavaScript
5
star
47

million-conference-slides

"Optimizing React for Performance with Million.js" conference slides for Million.js
JavaScript
5
star
48

handsfree-toolkit

A REPL with a composable Handsfree.js toolkit for developing webcam AI reverse workshops
JavaScript
4
star
49

stonks

Stock data viewer example w/ Million.js vs React
JavaScript
4
star
50

leetcode

๐Ÿงฉ My leetcode solutions
4
star
51

apcsp

๐Ÿ’ป Repository to store all my AP Computer Science Principles projects (2021-22)
TypeScript
4
star
52

jumpstart

โšก A simple Vite + Hacky boilerplate
TypeScript
4
star
53

folders

imagine website but folders
JavaScript
4
star
54

arewesamayet

Are we sama yet?
JavaScript
4
star
55

fiber-explorer

JavaScript
4
star
56

chinese-typer

๐Ÿงง Typer project for Chinese class.
JavaScript
4
star
57

sanic

๐Ÿ’ฌ Imagine messaging but ultrasonic
JavaScript
4
star
58

sheesh

๐Ÿคฉ Vite + Lucia + Tailwind starter that'll make you go SHEESH
TypeScript
4
star
59

fury

๐Ÿ˜ก Web-based FRC scouting application
JavaScript
4
star
60

calculus-adventure-project

Among Us Calc Adventure Project Web Interface
JavaScript
4
star
61

boomer

๐Ÿ‘ด Unopinionated monolithic webapp boilerplate
EJS
3
star
62

skyward-client

๐Ÿ’™๐Ÿ“˜ The better web client for Skyward
JavaScript
3
star
63

dns

๐Ÿ•น Manage Hack Club's DNS through a git repo
Shell
3
star
64

xu-ellen.github.io

๐ŸŒ Minimalistic personal website for Ellen Xu
HTML
3
star
65

lucia-demo

Lucia demos for ISEF
HTML
3
star
66

discordrp

CSS
3
star
67

albio

๐Ÿ›ฐ Astronomy package infrastructure for JavaScript
JavaScript
3
star
68

mst

๐Ÿง MST Magnet Research Journal
TypeScript
3
star
69

rap-god

the real slim shady
3
star
70

Quarters3

๐Ÿง  Online FRC 2471 game for small brain moments.
CSS
3
star
71

tion

Experimental tracking pixel for GitHub repositories
JavaScript
3
star
72

million-starter

๐Ÿ’ก Opinionated Million starter project
3
star
73

skyridge-science.github.io

:atom: We're a passionate group of students competing in Science Olympiad for Skyridge Middle School.
CSS
3
star
74

brotherblocker

super basic script to fake-block websites
JavaScript
3
star
75

apcsa-java-env

โ˜• Environment to use during AP test
Java
3
star
76

boilerplate

simple, no-bullshit fullstack web boilerplate
TypeScript
3
star
77

lucia-kumiko-starter

Starter project for a lucia/kumiko project
HTML
2
star
78

physics-final-project

TypeScript
2
star
79

mental-health-app

MST Freshman Mental Health App (Not by @aidenybai, repo for hosting)
HTML
2
star
80

yrs

yrs website
HTML
2
star
81

nn

neural net in javascript
2
star
82

remake-playground

My own playground with messing with remake
JavaScript
2
star
83

mst-database

๐Ÿง MST Magnet Research Journal (NEW)
JavaScript
2
star
84

deopt-web

Generate v8 logs for browser JavaScript
2
star
85

remake-cache-test

HTML
2
star
86

io

Socket.io server for personal use
JavaScript
2
star
87

nextjs-dev-server-temp

JavaScript
2
star
88

lucia-slide-deck

slide deck for symposium webinar
JavaScript
2
star
89

whapnomore

EJS
2
star
90

touch-grass

Touch grass, outerneter
JavaScript
1
star
91

lablab

1
star
92

swc-visitor-api

TypeScript
1
star
93

million-sierpinski-triangle-demo

Million.js implementation of Sierpinski Triangle React Fiber Demo
1
star
94

jacky

Directly use HTML inside JavaScript
1
star
95

semaphore

HTML
1
star
96

nextra-template

TypeScript
1
star