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

tsx-vanilla

v1.2.0

Published

Create DOM elements using JSX with excellent TypeScript support

Downloads

513

Readme

Bundle size NPM Package

tsx-vanilla

Create DOM elements using JSX with excellent TypeScript support

Installation

npm install tsx-vanilla

Setup

To use JSX, you need a compiler to transform the JSX into something the browser would understand. Easiest way to get this done is to use TypeScript with Vite and add this to your tsconfig.json.

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "element",
    "jsxFragmentFactory": "fragment"
  }
}

And then you include this import statement in every file where you want to use JSX.

import { element, fragment } from "tsx-vanilla"

Demo

open in stackblitz

Usage

import { element, fragment } from "tsx-vanilla"

// Functional components work as you'd expect
const Counter = (props: { start?: number }) => {
  let count = props.start || 0
  return <>
    <h2>Counter</h2>
    <button
      className="btn"
      onclick={function() {
        this.textContent = `count: ${++count}`
    }}>
      count: {count}
    </button>
  </>
}

// JSX expressions return DOM elements that can be appended directly to the DOM
document.body.append(
  <h1>tsx-vanilla</h1>,
  <Counter />,
  <Counter start={2} />
)

// Functional components are pure overhead
// Calling the function directly would be preferred
document.body.append(
  Counter({}),
  Counter({ start: 2 })
)

Attributes are set directly on the DOM element using something like this: element[key] = props[key]. This is the case for most attributes, but not:

  • style
  • attributes
  • children
  • ref
  • dataset
  • shadowRootOptions

style

style accepts either a string or an object. If it's a string, the style attribute is set to the value. If it's an object, all properties on that object are assigned to the CSSStyleDeclaration of the element. Custom properties are also accepted.

<h1 style="color: blue;">Blue text</h1>
<p style={{
  backgroundColor: "#eee",
  borderRadius: "0.5em",
  "--custom": "1em",
  padding: "var(--custom)"
}} >This is a styled paragraph.</p>

attributes

Not all attributes can be set as a property directly on the element. Therefore you can pass an object to attributes and all properties on that object will be set on the element using Element.setAttribute(). This can be used to set Aria-attributes or with SVG elements where most attributes have to be set with setAttribute()

dataset

dataset accepts an object which gets copied over to the element's dataset using Object.assign().

children

Assigning children using the children attribute is supported as long as the JSX-element doesn't also have nested elements. Children can be strings, numbers, nodes, or an array of the previously mentioned types. true, false, null and undefined are allowed to be passed as children, but will be filtered out.

When creating components accepting children, multiple children will be passed as an array, while single children won't. Something to also keep in mind when creating components is that the children array passed to your component won't be flattend.

ref

ref allows reference to an element without pulling it out of a nested node tree.

import { element, ref } from "tsx-vanilla"

// Can either be a ref object
const divRef = ref<HTMLDivElement>()

<div ref={divRef} />

console.log(divRef.value)

// Or a callback invoked after all attributes are set, but before being added to the DOM
<div ref={el => console.log(el)} />

shadowRootOptions

shadowRootOptions accepts an object which will get passed to Element.attachShadow() for the element. If this attribute is present, all children will be appended to the ShadowRoot instead of the element.

Other attributes

All properties that can be set on an element are supported. This includes textContent, innerHTML and innerText, but be careful. Attributes are set after children are appended. Therefore all children will be overridden. innerHTML can also make your site vulnerable to cross site scripting if the text is user-submitted.

SVG support

SVG support is not enabled by default to reduce bundle size for those who don't need it. To enable SVG support, you must import and call a function before creating any SVG elements with JSX.

import { element, addSVGSupport } from "tsx-vanilla"
addSVGSupport()

<svg attributes={{ viewBox: "0 0 5 4", width: 200, height: 160 }}>
  <circle attributes={{
    r: 1, cx: 2, cy: 2, fill: "blue", stroke: "black", "stroke-width": 0.1
  }} />
</svg>

All non-deprecated SVG elements except for those with the same name as an HTML element are supported. Since most attributes have to be set with setAttribute(), you need to use attributes a lot with SVG elements.

Using unsupported SVG elements

If you need to use non-supported SVG elements, you can include a function similar to this one to create a component.

import { element, appendChildren } from "tsx-vanilla"

function createSVGComponent(tagName: string) {
  return ({ children, ...attributes }: { children: JSX.Child | JSX.Children, [key: string]: any }) => {
    const el = document.createElementNS("http://www.w3.org/2000/svg", tagName)
    if (children) appendChildren([children], el)
    for (const attr in attributes) el.setAttribute(attr, attributes[attr])
    return el
  }
}

const SVGAnchor = createSVGComponent("a")

document.body.append(
  <svg attributes={{ viewBox: "0 0 5 4", width: 200, height: 160 }}>
    <SVGAnchor href="#">
      <circle attributes={{ r: 1, cx: 2, cy: 2, fill: "black" }} />
    </SVGAnchor>
  </svg>
)

MathML support

Just like with SVG, MathML elements are supported, but not by default to keep bundle sizes small.

import { element, addMathMLSupport } from "tsx-vanilla"

addMathMLSupport()
document.body.append(
  <math attributes={{ display: "block" }}>
    <msup>
      <mi>x</mi>
      <mn>2</mn>
    </msup>
  </math>
)

Can you use this without JSX?

Using this without JSX is fully supported, and in fact the returned elements will be typed better due to limitations of the JSX.Element type. The first JSX example could be rewritten like this without JSX:

import { element, fragment } from "tsx-vanilla"

const Counter = (props: { start?: number }) => {
  let count = props.start || 0
  return fragment({
    children: [
      element("h2", null, "Counter"),
      element("button", { 
        className: "btn",
        onclick() {
          this.textContent = `count: ${++count}`
        }
      }, "count: ", count)
    ]
  })
}

document.body.append(
  element("h1", null, "tsx-vanilla"),
  Counter({}),
  Counter({ start: 2 })
)

It's not even that clumbsy without JSX!

What is this meant for?

If you need a lightweight, fast and easy alternative to document.createElement() or innerHTML for creating elements on a website where a full framework isn't necessary, this might be the right fit. While this has very little overhead compared to using document.createElement() directly, it won't be as fast as cloning templates using Node.cloneNode(true) if you are reusing a lot of components.

Since this package is very small, there's no reactivity or state management. If you need to modify elements, you either have to recreate them every rerender or save a reference to them and do manual DOM manipulation if you want good performance.

Contributing

The purpose of this package is to be lightweight and I doubt any new features will be added unless they are very easy to implement. What would be appreciated is improving the types. It's likely that some attributes are missing or incorrectly typed and the types will need to be maintained as the web standards evolve in the future.