WebJSX

A minimal library for building web applications with Web Components and JSX.

It focuses on simplicity, and has just two core functions:

1
createElement:
Allows you to use JSX to write HTML markup. Returns Virtual Nodes.
2
applyDiff:
Merges Virtual Nodes created with JSX into the real DOM efficiently.

But Why?

In the last decade, we used React and React-like frameworks because Web Standards weren't there yet, and some of us wanted to write type-safe components. But Web Components are mature now, and if we combine that with JSX/TSX for type-safety we have everything we want, while staying close to Web Standards.

So let's keep JSX and get rid of everything else.

Examples

There are a few examples on StackBlitz. If you're impatient (like me), that's probably the easiest way to get started.

Installation

Install webjsx via npm:

npm install webjsx

Getting Started

The following is a basic example of how to use webjsx with its two main functions, createElement and applyDiff.

Creating Elements with JSX

WebJSX fully supports JSX syntax, allowing you to create virtual DOM elements using createElement and update the real DOM with applyDiff.

import * as webjsx from "webjsx";

// Define a simple virtual DOM element using JSX
const vdom = (
  <div id="main-container">
    <h1>Welcome to webjsx</h1>
    <p>This is a simple example.</p>
  </div>
);

// Select the container in the real DOM
const appContainer = document.getElementById("app");

// Apply the virtual DOM diff to update the real DOM
webjsx.applyDiff(appContainer, vdom);

Defining and Using Web Components with JSX

Let's write a simple Custom Element with JSX.

import * as webjsx from "webjsx";

// Define a custom Web Component
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ["title", "count"];
  }

  constructor() {
    super();
    this._count = 0;
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "title" || name === "count") {
      this.render();
    }
  }

  set count(val) {
    this._count = val;
    this.render();
  }

  get count() {
    return this._count;
  }

  render() {
    // Use webjsx's applyDiff to render JSX inside the Web Component
    const vdom = (
      <div>
        <h2>{this.getAttribute("title")}</h2>
        <p>Count: {this.count}</p>
      </div>
    );
    webjsx.applyDiff(this, vdom);
  }
}

// Register the custom element
if (!customElements.get("my-element")) {
  customElements.define("my-element", MyElement);
}

// Create a virtual DOM with the custom Web Component
const vdom = <my-element title="Initial Title" count={10}></my-element>;

// Render the custom Web Component
const appContainer = document.getElementById("app");
webjsx.applyDiff(appContainer, vdom);

Handling Events in JSX

Attach event listeners directly within your JSX using standard HTML event attributes.

import * as webjsx from "webjsx";

// Define an event handler
const handleClick = () => {
  alert("Button clicked!");
};

// Create a button with an onclick event
const vdom = <button onclick={handleClick}>Click Me</button>;

// Render the button
const appContainer = document.getElementById("app");
webjsx.applyDiff(appContainer, vdom);

Using Fragments

Group multiple elements without introducing additional nodes to the DOM using <>...</> syntax.

import * as webjsx from "webjsx";

// Define a custom Web Component using fragments
class MyList extends HTMLElement {
  connectedCallback() {
    const vdom = (
      <>
        <h2>My List</h2>
        <ul>
          <li>Item 1</li>
          <li>Item 2</li>
          <li>Item 3</li>
        </ul>
        <footer>Total items: 3</footer>
      </>
    );
    webjsx.applyDiff(this, vdom);
  }
}

// Register the custom element
if (!customElements.get("my-list")) {
  customElements.define("my-list", MyList);
}

// Render the custom Web Component
const appContainer = document.getElementById("app");
const vdom = <my-list></my-list>;
webjsx.applyDiff(appContainer, vdom);

API Reference

createElement(tag, props, children)

Creates a virtual DOM element.

JSX calls createElement implicitly:

const vdom = (
  <div id="main-container">
    <h1>Welcome to webjsx</h1>
  </div>
);

Usage (Non-JSX):

const vdom = webjsx.createElement(
  "div",
  { id: "main-container" },
  webjsx.createElement("h1", null, "Welcome to webjsx")
);

applyDiff(parent, newVirtualNode)

Applies the differences between the new virtual node(s) and the existing DOM.

Usage:

const vdom = <p class="text">Updated Text</p>;
webjsx.applyDiff(appContainer, vdom);

Fragment

A special type used to group multiple elements without adding extra nodes to the DOM.

Usage:

<>
  <span>Item 1</span>
  <span>Item 2</span>
</>

Example: Creating a Counter Web Component

import * as webjsx from "webjsx";

// Define the custom Web Component
class CounterElement extends HTMLElement {
  static get observedAttributes() {
    return ["title", "count"];
  }

  constructor() {
    super();
    this._count = 0;
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "title" || name === "count") {
      this.render();
    }
  }

  set count(val) {
    this._count = val;
    this.render();
  }

  get count() {
    return this._count;
  }

  render() {
    // Render JSX inside the Web Component
    const vdom = (
      <div>
        <h2>{this.getAttribute("title")}</h2>
        <p>Count: {this.count}</p>
        <button onclick={this.increment.bind(this)}>Increment</button>
      </div>
    );
    webjsx.applyDiff(this, vdom);
  }

  increment() {
    this.count += 1;
  }
}

// Register the custom element
if (!customElements.get("counter-element")) {
  customElements.define("counter-element", CounterElement);
}

// Create and render the CounterElement
const vdom = <counter-element title="My Counter" count={0}></counter-element>;

const appContainer = document.getElementById("app");
webjsx.applyDiff(appContainer, vdom);

TypeScript

tsconfig.json

Ensure your tsconfig.json is set up to handle JSX.

{
  "compilerOptions": {
    //...
    "jsx": "react",
    "jsxFactory": "webjsx.createElement",
    "jsxFragmentFactory": "webjsx.Fragment"
  }
}

Adding Custom Elements to IntrinsicElements (TypeScript)

TypeScript will complain that your Custom Element (such as <counter-element>) is not found. That's because it is only aware of standard HTML elements and doesn't know what <counter-element> is.

To fix this you need to declare custom elements in a declarations file, such as custom-elements.d.ts:

import "webjsx";

declare module "webjsx" {
  namespace JSX {
    interface IntrinsicElements {
      "counter-element": {
        count: number;
      };
      "sidebar-component": {
        about: string;
        email: string;
      };
    }
  }
}

Bundling

You can bundle with your favorite bundler, but most apps don't need to.

You can load modules directly on the web page these days:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>WebJsx Test</title>

    <script type="importmap">
      {
        "imports": {
          "webjsx": "../node_modules/webjsx/dist/index.js"
        }
      }
    </script>
    <!-- This is your entry point -->
    <script type="module" src="../dist/index.js"></script>
  </head>

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

You can see more examples in the StackBlitz.

Contributing

Contributions are welcome! Whether it's reporting bugs, suggesting features, or submitting pull requests, your help is appreciated. Please ensure that your contributions adhere to the project's coding standards and include appropriate tests.

To run the tests:

npm test

License

WebJSX is open-source software licensed as MIT.

Support

If you encounter any issues or have questions, feel free to open an issue on GitHub or reach out via Twitter @jeswin.