From b0007eba9d649b2327373c8800758e87b4e6edc5 Mon Sep 17 00:00:00 2001 From: Luke-zhang-04 Date: Sun, 7 Apr 2024 19:32:03 -0400 Subject: [PATCH] docs: document support for htm compat --- README.md | 16 +- docs/package.json | 3 +- docs/public/using-jsx.html | 217 +++++++++++++++-- docs/rollup.config.js | 1 + docs/src/examples/usingJsx.tsx | 141 +++++++++++ docs/src/pages.js | 2 +- pnpm-lock.yaml | 7 + test/cases/fragment.js | 3 +- test/type-sanity/elements.tsx | 12 + test/type-sanity/functionComponent.tsx | 315 +++++++++++++++++++++++++ wiki | 2 +- 11 files changed, 684 insertions(+), 35 deletions(-) create mode 100644 docs/src/examples/usingJsx.tsx create mode 100644 test/type-sanity/functionComponent.tsx diff --git a/README.md b/README.md index ad2e705a..10b5a1e1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ pnpm i destagnate Through `curl` or `wget` to download a bundle for browser usage
```bash -# Prodution +# Production curl -L https://unpkg.com/destagnate@/dist//deStagnate.min.js > deStagnate.js wget https://unpkg.com/destagnate@/dist//deStagnate.min.js @@ -63,7 +63,7 @@ With a CDN ## Kitchen Sink Example -Sorta. There's way more you can do with DeStagnate. See [https://luke-zhang-04.github.io/DeStagnate/docs](https://luke-zhang-04.github.io/DeStagnate/docs) for example code and documentation. +Sorta. There's way more you can do with DeStagnate. See [https://luke-zhang-04.github.io/DeStagnate](https://luke-zhang-04.github.io/DeStagnate) for example code and documentation. ```tsx const divRef = DeStagnate.createRef() @@ -151,11 +151,15 @@ container?.appendChild(button) ## Alternatives - What about [HTL](https://www.npmjs.com/package/htl)? - - HTL is cool, but it involves an HTML parser, which comes with its drawbacks. One upside though, you don't need to transpile to create DOM using XML-like syntax. -- What about [HTM](https://www.npmjs.com/package/htm)? - - HTM generates virtual DOM and doesn't directly create DOM nodes. + - HTL is cool, but it involves an HTML parser at runtime, which makes it quite slow. You also lose devtool support whren working with strings. One upside though, you don't need to transpile to create DOM using XML-like syntax. +- What about [VHTML](https://github.com/developit/vhtml)? + - VHTML generates strings which you can safely add to the DOM with `innerHTML`. Unfortunately this has the drawback of not supporting refs or event listeners, which is extremely limiting. - Why not just `innerHTML`? - - You're missing dev tool support, it's a big security risk, and you'll have to deal with character escaping. Not fun. Mike Bostock goes over why `innerHTML` is bad in the [HTL README](https://www.npmjs.com/package/htl). + - You're missing dev tool support, it's a big security risk, and you'll have to deal with character escaping. Not fun. Mike Bostock goes over why `innerHTML` is bad in the [HTL README](https://github.com/observablehq/htl?tab=readme-ov-file#why-not-concatenate). + +## Using HTM + +Since [HTM](https://www.npmjs.com/package/htm) is meant to be a drop-in replacement wherever JSX is used, DeStagnate is compatible with `HTM`. See [using-jsx](https://luke-zhang-04.github.io/DeStagnate/using-jsx.html#using-htm). ## Using JSX diff --git a/docs/package.json b/docs/package.json index 040d95b3..b47241e6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,7 +19,8 @@ }, "keywords": [], "dependencies": { - "bootstrap": "~5.0.0-alpha1" + "bootstrap": "~5.0.0-alpha1", + "htm": "^3.1.1" }, "devDependencies": { "sass": "^1.26.10", diff --git a/docs/public/using-jsx.html b/docs/public/using-jsx.html index a8ae132f..bd693f6e 100644 --- a/docs/public/using-jsx.html +++ b/docs/public/using-jsx.html @@ -16,7 +16,7 @@ -
+

Using JSX

If you're using JSX, you'll need a transpiler. Either TypeScript, or a Babel with a Plugin @@ -33,17 +33,17 @@

Steps

  1. Install TypeScript (if you haven't already)
  2. -                
    +          
     # npm
     npm i typescript -D
     
     # yarn
     yarn add typescript -D
    -                
    -            
    + +
  3. Create and configure your tsconfig file
  4. -                
    +          
     {
         "compilerOptions": {
             "jsx": "react",
    @@ -52,24 +52,24 @@ 

    Steps

    }, "include": ["myFile.tsx"], } -
    -
    + +
  5. Include DeStagnate in your files that use JSX (if you haven't already)
  6. -                
    +          
     import DeStagnate from "destagnate" // Include this, even if linters say it's never used
     
     document.getElementById("id")?.appendChild(
         <div></div>
     )
    -                
    -            
    + +
  7. Compile
  8. -                
    +          
     npx tsc
    -                
    -            
    + +

Babel

@@ -81,18 +81,18 @@

Steps

  1. Install Babel and plugins
  2. -                
    +          
     # npm
     npm i @babel/core @babel/cli @babel/plugin-transform-react-jsx -D
     
     # yarn
     yarn add @babel/core @babel/cli @babel/plugin-transform-react-jsx -D
    -                
    -            
    + +
  3. Create and configure your .babelrc file
  4. -                
    +          
     module.exports = {
         "plugins": [
             [
    @@ -104,27 +104,190 @@ 

    Steps

    ], ], } -
    -
    + +
  5. Include DeStagnate in your files that use JSX (if you haven't already)
  6. -                
    +          
     import DeStagnate from "destagnate" // Include this, even if linters say it's never used
     
     document.getElementById("id")?.appendChild(
         <div></div>
     )
    -                
    -            
    + +
  7. Compile
  8. -                
    +          
     npx babel myfile.tsx -o myfile.js
    -                
    -            
    + +
+ +

Using HTM

+

Since HTM is a drop-in replacement wherever JSX is used, DeStagnate is compatible with HTM + as well.

+ +

Example

+

Here is creating the same table using createElement, JSX, HTM, and vanilla DOM.

+ Source Code +
+
+
+
+        
+import * as DeStagnate from "../../../"
+import {createElement} from "../../../"
+import htm from "htm"
+
+const data = new Array(10).fill(undefined).map((_, index) => index)
+const container = document.getElementById("jsx-example")!
+
+interface TableData {
+    num: number
+}
+
+jsx: {
+    const Row: DeStagnate.FC<TableData, []> = ({num}) => (
+        <tr>
+            <td>{num}</td>
+            <td>{num % 2 === 0 ? "Even" : "Odd"}</td>
+            <td>{(num % 3 === 0).toString()}</td>
+        </tr>
+    )
+
+    container.appendChild(
+        <div class="col-3">
+            <table>
+                <tr>
+                    <th>Number</th>
+                    <th>Even/odd</th>
+                    <th>Multiple of 3</th>
+                </tr>
+                {data.map((num) => (
+                    <Row num={num} />
+                ))}
+            </table>
+        </div>,
+    )
+}
+
+createElement: {
+    const Row: DeStagnate.FC<TableData, []> = ({num}) =>
+        createElement(
+            "tr",
+            null,
+            createElement("td", null, num),
+            createElement("td", null, num % 2 === 0 ? "Even" : "Odd"),
+            createElement("td", null, (num % 3 === 0).toString()),
+        )
+
+    container.appendChild(
+        createElement(
+            "div",
+            {class: "col-3"},
+            createElement(
+                "table",
+                null,
+                createElement(
+                    "tr",
+                    null,
+                    createElement("th", null, "Number"),
+                    createElement("th", null, "Even/odd"),
+                    createElement("th", null, "Multiple of 3"),
+                ),
+                data.map((num) => createElement(Row, {num})),
+            ),
+        ),
+    )
+}
+
+vanillaDOM: {
+    const row = (num: number): HTMLTableRowElement => {
+        const tableRow = document.createElement("tr")
+
+        const [d1, d2, d3] = [
+            document.createElement("td"),
+            document.createElement("td"),
+            document.createElement("td"),
+        ]
+
+        d1.innerText = num.toString()
+        d2.innerText = num % 2 === 0 ? "Even" : "Odd"
+        d3.innerText = (num % 3 === 0).toString()
+
+        tableRow.appendChild(d1)
+        tableRow.appendChild(d2)
+        tableRow.appendChild(d3)
+
+        return tableRow
+    }
+
+    const col = document.createElement("div")
+
+    col.setAttribute("class", "col-3")
+
+    const table = document.createElement("table")
+
+    const headerRow = document.createElement("tr")
+    const [numberHeader, evenOddHeader, mult3Header] = [
+        document.createElement("th"),
+        document.createElement("th"),
+        document.createElement("th"),
+    ]
+
+    numberHeader.innerText = "Number"
+    evenOddHeader.innerText = "Even/odd"
+    mult3Header.innerText = "Multiple of 3"
+
+    headerRow.appendChild(numberHeader)
+    headerRow.appendChild(evenOddHeader)
+    headerRow.appendChild(mult3Header)
+
+    table.appendChild(headerRow)
+
+    for (const num of data) {
+        table.appendChild(row(num))
+    }
+
+    col.appendChild(table)
+    container.appendChild(col)
+}
+
+htm: {
+    const html = htm.bind(createElement)
+
+    const Row: DeStagnate.FC<TableData, []> = ({num}) =>
+        html`<tr>
+            <td>${num}</td>
+            <td>${num % 2 === 0 ? "Even" : "Odd"}</td>
+            <td>${(num % 3 === 0).toString()}</td>
+        </tr>` as Element
+
+    container.appendChild(
+        html`<div class="col-3">
+            <table>
+                <tr>
+                    <th>Number</th>
+                    <th>Even/odd</th>
+                    <th>Multiple of 3</th>
+                </tr>
+                ${data.map((num) => html`<${Row} num=${num} />`)}
+            </table>
+        </div>` as Element,
+    )
+}
+        
+      
@@ -147,5 +310,9 @@

Steps

+ + + + diff --git a/docs/rollup.config.js b/docs/rollup.config.js index 423ebf75..75c8f44b 100644 --- a/docs/rollup.config.js +++ b/docs/rollup.config.js @@ -27,4 +27,5 @@ export default [ "examples/eventListener.js", "examples/svg.js", "examples/tictactoe.js", + "examples/usingJsx.js", ].map(createConfig) diff --git a/docs/src/examples/usingJsx.tsx b/docs/src/examples/usingJsx.tsx new file mode 100644 index 00000000..ca24abb9 --- /dev/null +++ b/docs/src/examples/usingJsx.tsx @@ -0,0 +1,141 @@ +import * as DeStagnate from "../../../" +import {createElement} from "../../../" +import htm from "htm" + +const data = new Array(10).fill(undefined).map((_, index) => index) +const container = document.getElementById("jsx-example")! + +interface TableData { + num: number +} + +jsx: { + const Row: DeStagnate.FC = ({num}) => ( + + {num} + {num % 2 === 0 ? "Even" : "Odd"} + {(num % 3 === 0).toString()} + + ) + + container.appendChild( +
+ + + + + + + {data.map((num) => ( + + ))} +
NumberEven/oddMultiple of 3
+
, + ) +} + +createElement: { + const Row: DeStagnate.FC = ({num}) => + createElement( + "tr", + null, + createElement("td", null, num), + createElement("td", null, num % 2 === 0 ? "Even" : "Odd"), + createElement("td", null, (num % 3 === 0).toString()), + ) + + container.appendChild( + createElement( + "div", + {class: "col-3"}, + createElement( + "table", + null, + createElement( + "tr", + null, + createElement("th", null, "Number"), + createElement("th", null, "Even/odd"), + createElement("th", null, "Multiple of 3"), + ), + data.map((num) => createElement(Row, {num})), + ), + ), + ) +} + +vanillaDOM: { + const row = (num: number): HTMLTableRowElement => { + const tableRow = document.createElement("tr") + + const [d1, d2, d3] = [ + document.createElement("td"), + document.createElement("td"), + document.createElement("td"), + ] + + d1.innerText = num.toString() + d2.innerText = num % 2 === 0 ? "Even" : "Odd" + d3.innerText = (num % 3 === 0).toString() + + tableRow.appendChild(d1) + tableRow.appendChild(d2) + tableRow.appendChild(d3) + + return tableRow + } + + const col = document.createElement("div") + + col.setAttribute("class", "col-3") + + const table = document.createElement("table") + + const headerRow = document.createElement("tr") + const [numberHeader, evenOddHeader, mult3Header] = [ + document.createElement("th"), + document.createElement("th"), + document.createElement("th"), + ] + + numberHeader.innerText = "Number" + evenOddHeader.innerText = "Even/odd" + mult3Header.innerText = "Multiple of 3" + + headerRow.appendChild(numberHeader) + headerRow.appendChild(evenOddHeader) + headerRow.appendChild(mult3Header) + + table.appendChild(headerRow) + + for (const num of data) { + table.appendChild(row(num)) + } + + col.appendChild(table) + container.appendChild(col) +} + +htm: { + const html = htm.bind(createElement) + + const Row: DeStagnate.FC = ({num}) => + html` + ${num} + ${num % 2 === 0 ? "Even" : "Odd"} + ${(num % 3 === 0).toString()} + ` as Element + + container.appendChild( + html`
+ + + + + + + ${data.map((num) => html`<${Row} num=${num} />`)} +
NumberEven/oddMultiple of 3
+
` as Element, + ) +} diff --git a/docs/src/pages.js b/docs/src/pages.js index b7eb9749..459d9242 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -3,6 +3,6 @@ export default [ ["eventListener.html", "Event listener example"], ["calculator.html", "Calculator Example"], ["namespace.html", "Namespaced element (SVG) example"], - ["using-jsx.html", "Using JSX"], + ["using-jsx.html", "Using JSX and HTM"], ["docs/", "Documentation"], ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ab47408..f29ba3ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: bootstrap: specifier: ~5.0.0-alpha1 version: 5.0.0-alpha1(popper.js@1.16.1) + htm: + specifier: ^3.1.1 + version: 3.1.1 devDependencies: sass: specifier: ^1.26.10 @@ -2858,6 +2861,10 @@ packages: lru-cache: 7.18.3 dev: true + /htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + dev: false + /html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} diff --git a/test/cases/fragment.js b/test/cases/fragment.js index 33247a65..8e8dac62 100644 --- a/test/cases/fragment.js +++ b/test/cases/fragment.js @@ -7,7 +7,8 @@ export const test = () => { Fragment, null, createElement("div", {id: "fragmentTest1"}), - createElement("div", {id: "fragmentTest2"}), + createElement(Fragment, undefined, createElement("div", {id: "fragmentTest2"})), + createElement(Fragment), ), ) diff --git a/test/type-sanity/elements.tsx b/test/type-sanity/elements.tsx index d9664d88..eb7116ec 100644 --- a/test/type-sanity/elements.tsx +++ b/test/type-sanity/elements.tsx @@ -44,3 +44,15 @@ createElement("a", {href: true}) createElement("a", null,
) ;{createElement("div")} + +div = createElement( + "div", + {class: "col-3"}, + createElement("table", null, createElement("tr", null), [createElement("tr", null)]), +) +;
+ + + {[]} +
+
diff --git a/test/type-sanity/functionComponent.tsx b/test/type-sanity/functionComponent.tsx new file mode 100644 index 00000000..85e42972 --- /dev/null +++ b/test/type-sanity/functionComponent.tsx @@ -0,0 +1,315 @@ +import * as DeStagnate from "../../src" +import {createElement} from "../../src" + +let node: Node + +interface CustomProps { + obj: { + prop: number + } + arr: number[] + uintArr: Uint32Array + buffer: Buffer + opt?: string +} + +{ + const Component: DeStagnate.FC = ({obj, arr, uintArr, buffer, opt}) => + createElement("div", null, {obj, arr, uintArr, buffer, opt}.toString()) + + // @ts-expect-error + createElement(Component) + // @ts-expect-error + createElement(Component, {}) + // @ts-expect-error + ; + + node = createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + }) + ; + node = createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + opt: "", + }) + ; + + // @ts-expect-error + createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + }) + // @ts-expect-error + ; + + createElement(Component, { + obj: {prop: 0}, + arr: [], + // @ts-expect-error + uintArr: new Uint16Array(), + buffer: Buffer.from([]), + }) + // @ts-expect-error + ; +} + +{ + const Component: DeStagnate.FC = ( + {obj, arr, uintArr, buffer, opt}, + ...children + ) => createElement("div", null, {obj, arr, uintArr, buffer, opt}.toString(), children) + + // @ts-expect-error + createElement(Component) + // @ts-expect-error + createElement(Component, {}) + // @ts-expect-error + ; + + node = createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + }) + ; + node = createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + opt: "", + }) + ; + + // @ts-expect-error + createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + }) + // @ts-expect-error + ; + + createElement(Component, { + obj: {prop: 0}, + arr: [], + // @ts-expect-error + uintArr: new Uint16Array(), + buffer: Buffer.from([]), + }) + // @ts-expect-error + ; +} + +{ + const Component: DeStagnate.FC = ( + {obj, arr, uintArr, buffer, opt}, + ...children + ) => createElement("div", null, {obj, arr, uintArr, buffer, opt}.toString(), children) + + // @ts-expect-error + createElement(Component) + // @ts-expect-error + createElement(Component, {}) + // @ts-expect-error + ; + + node = createElement( + Component, + { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + }, + "", + 0, + ) + ; + {""} + {0} + + node = createElement( + Component, + { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + opt: "", + }, + "", + 0, + ) + ; + {""} + {0} + + + // @ts-expect-error + createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + }) + // @ts-expect-error + ; + {""} + {0} + + createElement( + Component, + { + obj: {prop: 0}, + arr: [], + // @ts-expect-error + uintArr: new Uint16Array(), + buffer: Buffer.from([]), + }, + "", + 0, + ) + // @ts-expect-error + ; + {""} + {0} + + + node = createElement( + // @ts-expect-error + Component, + { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + opt: "", + }, + createElement("div"), + ) + // Unfortunately, Typescript does not check if the JSX children type is valid, so this passes with no problem + ; +
+ +} + +{ + const Component: DeStagnate.FC = ( + {obj, arr, uintArr, buffer, opt}, + ...children + ) => createElement("div", null, {obj, arr, uintArr, buffer, opt}.toString(), children) + + const Component2: DeStagnate.FC = ( + {obj, arr, uintArr, buffer, opt}, + ...children + // @ts-expect-error + ) => createElement("a", null, {obj, arr, uintArr, buffer, opt}.toString(), children) + + // @ts-expect-error + createElement(Component) + // @ts-expect-error + createElement(Component, {}) + // @ts-expect-error + ; + + let div: HTMLDivElement + + div = createElement( + Component, + { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + }, + "", + 0, + ) + ; + {""} + {0} + + div = createElement( + Component, + { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + buffer: Buffer.from([]), + opt: "", + }, + "", + 0, + ) + ; + {""} + {0} + + + // @ts-expect-error + createElement(Component, { + obj: {prop: 0}, + arr: [], + uintArr: new Uint32Array(), + }) + // @ts-expect-error + ; + {""} + {0} + + createElement( + Component, + { + obj: {prop: 0}, + arr: [], + // @ts-expect-error + uintArr: new Uint16Array(), + buffer: Buffer.from([]), + }, + "", + 0, + ) + // @ts-expect-error + ; + {""} + {0} + +} diff --git a/wiki b/wiki index 2800b4cb..1a51b68e 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 2800b4cb5bdbdbe560fc4ef7906f944aa47d1a90 +Subproject commit 1a51b68e66d6d1bdf365ce33907b91157b6cfb54