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

vdom-mini

v1.0.1

Published

Like an iPad Mini, but instead of a tablet, it's a 250-line virtual DOM implementation from scratch.

Downloads

17

Readme

Setup

You can import the script using any CDN of your choice:

<script src="https://cdn.jsdelivr.net/npm/vdom-mini@latest"></script>

From there, you need to declare an empty element to mount your virtual DOM to. This element will be preserved and only the children inside will be modified.

<div id="app"></div>

Now you can reference this element when creating a new Virtual DOM instance:

const app = new VirtualDOM({
  el: "#app",
  view(h) {
    return h("h1", null, "Hello from the virtual DOM!");
  },
});
// mount the app to the selector specified in the options object
// you can also provide a different selector as the first parameter
app.mount();

Templates

Templates are written using a React-like createElement function (called h for brevity), and exported using the view() function. This is the only required option in the VirtualDOM constructor.

const app = new VirtualDOM({
  view(h) {
    // only the first argument is required
    // children can be any combination of strings and other function calls
    return h("div", { class: "container" }, [
      h("h1", { id: "title" }, "This is a title"),
      h("p", null, "This is text"),
    ]);
  },
});
// the function argument takes priority if both are specified
app.mount("#app");

State

You can add state to your app using the state() method, which will be run before the DOM is initialized. The first parameter will be a reactivity factory function, which by convention will be referred to as $.

new VirtualDOM({
  state($) {
    const title = $("This is a title");
    return { title };
  },
  // all data returned from state is available here using this
  view(h) {
    // data is retrieved using the value getter on each returned reactive object
    return h("h1", null, this.title.value);
  },
});

Derived state is returned from the same state hook, along with any functions:

new VirtualDOM({
  state($) {
    const title = $("This is a title");
    // functions inside the reactivity factory function are interpreted as derived state
    const screamingTitle = $(() => title.value.toUpperCase());
    // functions are declared as normal
    function updateTitle() {
      title.value = "Another title";
    }
    // everything is returned at the end
    return { title, screamingTitle, updateTitle };
  },
  // now, all state, derived values, and functions can be accessed through this
  view(h) {
    return h("div", { class: "container" }, [
      h("h1", null, this.screamingTitle.value),
      // event listeners are cased as myevent -> onMyevent
      h(
        "button",
        {
          onClick: () => updateTitle(),
        },
        "Click to update title",
      ),
      this.title.value === "This is a title" &&
        h(
          "p",
          null,
          "Conditionally render children using the && operator",
        ),
    ]);
  },
});

Effects are stored in a watch object.

new VirtualDOM({
  state($) {
    const title = $("my value");
    return { title };
  },
  watch: {
    title(newValue) {
      // shows "Title was changed to another value!" on button click
      alert(`Title was changed to ${newValue}!`);
    },
  },
  view(h) {
    return h("div", { class: "container" }, [
      h("h1", null, this.title.value),
      h("button",
        {
          onClick: () => {
            this.title.value = "another value"
          }
        }
      );
    ]);
  },
});

A more Vue.js-like option-based API is available as well:

new VirtualDOM({
  data() {
    return {
      title: "my value",
    };
  },
  methods: {
    updateTitle() {
      // you still have to use .value for getters/setters
      this.title.value = "another title";
    },
  },
  computed: {
    screamingTitle() {
      return this.title.value.toUpperCase();
    },
  },
  view(h) {
    // all state is still available in this
    return h("h1", null, this.screamingTitle.value);
  },
});

Lifecycle Hooks

Lifecycle hooks are available using a similar convention to Vue.js's lifecycle hooks:

new VirtualDOM({
  created() {
    // this runs when state is initialized
    // only state is available here
  },
  mounted() {
    // this runs when the DOM has been mounted to the selector
    // DOM methods like querySelector and getElementById are also available here
  },
  updated() {
    // this runs after the DOM has been updated
    // so DOM methods will point to the latest version
  },
});

Reactivity Implementation

Reactivity is implemented using ES5 getters and setters. When a value is set, it emits an internal signal that rerenders the result of the view function to the actual DOM, and calls any associated watchers once the DOM has been diffed against the previous view function result and reconcilliated with the actual DOM.

The $ function has two different operations depending on the type of the object passed as the parameter. If the parameter is a regular value, the function returns a reactive object with both a getter and setter for value. However, if the parameter itself is a function, the function returns a getter-only object that calls the parameter when accessed. Since the function is polymorphic and therefore the only necessary import to declare all state variables, it's passed in the state function call directly to save on code.

// simplified state object representation
({
  _emit() {},
  _value: val,
  get value() {
    return this._value;
  },
  set value(nv) {
    this._value = nv;
    // this will be described more later
    this._emit();
  },
});
// simplified derived object representation
({
  _fn: val,
  get value() {
    // ensure latest version is always received
    return this._fn();
  },
});

Virtual DOM Node Implementation

The h function is simply syntactic sugar to easily build a tree of virtual DOM nodes—you can return a series of plain objects in the view function as well for the same behavior if you really wanted to.

h("h1", { class: "heading" }, "This is some text");
// returns
({
  tag: "h1",
  props: { class: "heading" },
  children: ["This is some text"],
});

Note how children must be an array in the raw object, where in the function non-array children are automatically converted.

Combining VNodes and Reactivity

The VirtualDOM class combines these two different functions ($ and h) into one unified interface. The state function (or equivalent data objects) handle reactivity and the view function handles DOM nodes, with the class itself serving as the bridge handling reconcilliating, diffing, and dispatching the correct signals at the correct time.

The fundamental link between these two worlds comes through the _emit function in a state object. This mysterious value is set when the state function gets called, with each state value having its emit function set to rerender the DOM when called.

// state is stored as a field on the VirtualDOM instance called $data
this.$data[k]._emit = () => {
  // use cached result from the previous render to diff the changes
  const prevNodes = this._vNodeCache;
  // save new result to the now-outdated vnode cache for the next render
  this._vNodeCache = this.$view(createElement);
  // diff the outdated and rerendered nodes and apply changes to the actual DOM
  rerender(this.$el, prevNodes, this._vNodeCache);
  // handle watchers and lifecycle hooks
  this.onUpdate();
  this.$watchers[k](this.$data);
};

Now that setting a value rerenders the DOM and applies only the necessary changes, this is effectively the bare bones of the entire virtual DOM done. There isn't an extensive component system like other frameworks, but for adding simple reactivity to a project in a declarative way, this project is a fairly reasonable option.