{"id":24083289,"url":"https://github.com/mitranim/prax","last_synced_at":"2025-10-19T16:15:05.727Z","repository":{"id":48559624,"uuid":"44992729","full_name":"mitranim/prax","owner":"mitranim","description":"Experimental rendering library geared towards hybrid SSR+SPA apps. Focus on radical simplicity and performance. Tiny and dependency-free.","archived":false,"fork":false,"pushed_at":"2022-02-15T16:19:47.000Z","size":1805,"stargazers_count":21,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-11T14:55:34.842Z","etag":null,"topics":["dom","javascript","js","jsx","performance","rendering","simplicity","spa","ssr","synchronous","ui","view"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mitranim.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-10-26T19:40:38.000Z","updated_at":"2023-09-04T21:05:53.000Z","dependencies_parsed_at":"2022-08-27T22:03:25.989Z","dependency_job_id":null,"html_url":"https://github.com/mitranim/prax","commit_stats":null,"previous_names":[],"tags_count":124,"template":false,"template_full_name":null,"purl":"pkg:github/mitranim/prax","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fprax","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fprax/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fprax/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fprax/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mitranim","download_url":"https://codeload.github.com/mitranim/prax/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fprax/sbom","scorecard":{"id":95117,"data":{"date":"2022-08-15","repo":{"name":"github.com/mitranim/prax","commit":"5c01224a44bc78459b2e99866d3f59572a3777ba"},"scorecard":{"version":"v4.5.0-17-g7772984","commit":"777298477c07c262a4ec7e95ceee839b7b3b75ae"},"score":4.6,"checks":[{"name":"Code-Review","score":1,"reason":"GitHub code reviews found for 4 commits out of the last 30 -- score normalized to 1","details":["Warn: no reviews found for commit: 5c01224a44bc78459b2e99866d3f59572a3777ba","Warn: no reviews found for commit: 694544d545e8141bc390df7b979c452f56ececd5","Warn: no reviews found for commit: 8f9fc1bf960854e9fa5290302502594ccbaa4a03","Warn: no reviews found for commit: 93496c894b7893b1c022360724dcbd8bba8353f4","Warn: no reviews found for commit: 5ad70a31e3d467ef4e5142d2d08f7293d06e7388","Warn: no reviews found for commit: 9ef3e2174e80c9a8c546f18ba429fa523896f751","Warn: no reviews found for commit: 51959d1ca2f724c962e3b1eee03a8e36f4ffb6ff","Warn: no reviews found for commit: ccfa8296d333629afb86540c47a408b16813dabf","Warn: no reviews found for commit: ff87f36ad35fdd100dda87b7bea8a6d08bd14a4b","Warn: no reviews found for commit: dc765ba421eea7d4f8e5df92b4d0cfe16ffb0f25","Warn: no reviews found for commit: c61aa31242e703ae0c2e704012d68d05b82c705a","Warn: no reviews found for commit: 1ea445e2612e4101b29911411e36f6bc702045cc","Warn: no reviews found for commit: 29ba0e793a3826f6ef7cb6456e57b5b52d62cb41","Warn: no reviews found for commit: f1b1fec648ad39efe2efc39147d026760cc54ab7","Warn: no reviews found for commit: 8c8e750a48aef8c84a9c1664e227781e4328500f","Warn: no reviews found for commit: d5bf8cd416e77c5f8b56c59e2b269ff83a48cf37","Warn: no reviews found for commit: 9efa1593346cf3fe3c516e2e3ae460d643c4e004","Warn: no reviews found for commit: 5c88ef62ad14895266a08eff7b52917548f75f6c","Warn: no reviews found for commit: 966fcc159652f37abacec827d5ee6739fb48b954","Warn: no reviews found for commit: 12bbc7946c8982eceaf8915916d09049e8e35de9","Warn: no reviews found for commit: 4fb90f0f44e2d476d5e9a9351e60b84a5bb58ded","Warn: no reviews found for commit: 561fcf56758ef21ff4a6b653a3257a8d2a06089b","Warn: no reviews found for commit: c4e759efb2959c56ad400193c408c659584744fb","Warn: no reviews found for commit: 1ff0b52ef64dd82f5a072b719c81c0ebd9ea42e3","Warn: no reviews found for commit: f96654924a8b863cb365213b9511583e42ce866d","Warn: no reviews found for commit: 2f6366b9a7a3960494b275772a4a7ffa90ede8be"],"documentation":{"short":"Determines if the project requires code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) out of 30 and 0 issue activity out of 1 found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no badge detected","details":null,"documentation":{"short":"Determines if the project has a CII Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"no vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#vulnerabilities"}},{"name":"Token-Permissions","score":10,"reason":"tokens are read-only in GitHub workflows","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"no published package detected","details":["Warn: no GitHub publishing workflow detected"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info: GitHub-owned GitHubActions are pinned","Info: Third-party GitHubActions are pinned","Info: Dockerfile dependencies are pinned","Info: no insecure (not pinned by hash) dependency downloads found in Dockerfiles","Info: no insecure (not pinned by hash) dependency downloads found in shell scripts"],"documentation":{"short":"Determines if the project has declared and pinned its dependencies.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#pinned-dependencies"}},{"name":"License","score":0,"reason":"license file not detected","details":null,"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#license"}},{"name":"Dependency-Update-Tool","score":0,"reason":"no update tool detected","details":["Warn: dependabot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others.","Warn: renovatebot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others."],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#dependency-update-tool"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":["Warn: no GitHub releases found"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#branch-protection"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":null,"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":null,"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#security-policy"}}]},"last_synced_at":"2025-08-15T08:49:12.276Z","repository_id":48559624,"created_at":"2025-08-15T08:49:12.276Z","updated_at":"2025-08-15T08:49:12.276Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279847886,"owners_count":26234923,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-19T02:00:07.647Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","javascript","js","jsx","performance","rendering","simplicity","spa","ssr","synchronous","ui","view"],"created_at":"2025-01-09T23:56:17.745Z","updated_at":"2025-10-19T16:15:05.698Z","avatar_url":"https://github.com/mitranim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Overview\n\nHTML/DOM rendering system for hybrid SSR + SPA apps. See [#Why](#why). In short: performance and _radical_ simplicity.\n\n* Markup = nested function calls.\n* No intermediate representations.\n  * Render directly to strings in Deno/Node.\n  * Render directly to DOM nodes in browsers.\n* No VDOM.\n* No diffing (mostly).\n* No library classes.\n* No templates.\n* No string parsing.\n* Render only once. Use native [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) for state. (Custom elements don't need library support.)\n* Replace instead of reflowing.\n* Runs in [Node](https://nodejs.org/en/about/), [Deno](https://deno.land), and browsers.\n* Nice-to-use in plain JS. No build system required.\n\nTiny (a few kilobytes unminified) and dependency-free. Native JS modules.\n\n## TOC\n\n* [#Why](#why)\n  * [#Why not React?](#why-not-react)\n  * [#Why not Svelte?](#why-not-svelte)\n  * [#Why not plain strings?](#why-not-plain-strings)\n  * [#Why not framework X?](#why-not-framework-x)\n* [#Usage](#usage)\n* [#API](#api)\n  * [#Isomorphic](#isomorphic)\n    * [#`E`](#etype-props-children)\n    * [#`S`](#stype-props-children)\n    * [#`F`](#fchildren)\n    * [#`cls`](#clsvals)\n    * [#`len`](#lenchildren)\n    * [#`vac`](#vacchildren)\n    * [#`map`](#mapchildren-fun-args)\n    * [#`doc`](#docval)\n    * [#`merge`](#mergevals)\n    * [#`lax`](#laxval)\n    * [#`e`](#etype-props-children-1)\n  * [#`dom.mjs`](#dommjs)\n    * [#`reset`](#resetelem-props-children)\n    * [#`resetProps`](#resetpropselem-props)\n    * [#`replace`](#replacenode-children)\n    * [#`resetDoc`](#resetdochead-body)\n    * [#`resetHead`](#resetheadhead)\n    * [#`resetBody`](#resetbodybody)\n    * [#`resetText`](#resettextnode-src)\n    * [#`props`](#propsnode)\n  * [#`str.mjs`](#strmjs)\n  * [#`reg.mjs`](#regmjs)\n    * [#Base Classes](#base-classes)\n    * [#`reg`](#regcls)\n  * [#`rcompat.mjs`](#rcompatmjs)\n  * [#Undocumented](#undocumented)\n* [#Imperative, Synchronous](#imperative-synchronous)\n* [#Direct Instantiation](#direct-instantiation)\n* [#Props](#props)\n* [#Children](#children)\n* [#Stringable](#stringable)\n* [#JSX](#jsx)\n\n## Why\n\n### Why not React?\n\n#### Bad for SSR+SPA\n\nReact seems _particularly unfit_ for hybrid SSR + SPA apps. Your ideal flow:\n\n* On any request, render the _entire_ page in Deno/Node.\n* The user gets the fully-built content. It has no JS placeholders, doesn't require any ajax, and doesn't contain invisible JSON.\n* Load the same JS into the browser. Enable interactive components and pushstate links. No initial ajax.\n* On pushstate transitions, fetch data and render pages entirely in the browser. The rendering code is fully isomorphic between Deno/Node and browsers.\n\nIf the entire page was rendered with React, then to activate the interactive parts, you must load all that JS and _re-render_ the entire page with React. In the browser. On a page that was already rendered. This is ludicrously wasteful. In my current company's app, this easily takes 500ms of CPU time on many pages (using Preact). To make it worse, because markup is a function of data (regardless of your rendering library), this requires the _original data_, which you must either invisibly inline into HTML (wasted traffic, SEO deranking), or re-fetch in the browser (wasted traffic, server load). This is insane.\n\nFor apps with SSR, a better approach is to activate interactive components in-place without any \"render\". Native custom elements are _particularly fit_ for this. This requires separating the _markup_ code from the _stateful behavior_ code. React is predicated on the idea of co-locating markup and state. You could still render your markup with React, but when markup is stateless, this is pointlessly inefficient. Prax can do it much faster, while optionally using JSX.\n\n#### Slow\n\nReact's rendering model is _inefficient by design_. Svelte's creator did a better job explaining this: https://svelte.dev/blog/virtual-dom-is-pure-overhead.\n\nThe _design_ inefficiencies described above shouldn't apply to the initial page render in Deno/Node, when there's nothing to diff against. However, there are also needless _implementation_ inefficiencies. My benchmarks are for Preact, which we've been using over React for size reasons. At my current company, replacing `preact@10.5.13` and `preact-render-to-string@5.1.18` with the initial, naive and almost unoptimized Prax yields x5 better performance in Node, and somewhere between x3 and x8 (depending on how you measure) better in browsers, for rendering big pages. In addition, switching to custom elements for stateful components allows to completely eliminate the in-browser initial render, which would hitch the CPU for 500ms on some pages, while also eliminating the need to fetch or inline a huge amount of data that was required for that render.\n\nParts of the overhead weren't in Preact itself, but merely encouraged by it. In SSR, components would initialize state, create additional owned objects, bind callbacks, and establish implicit reactive subscriptions, even though this would all be wasted. This doesn't excuse an unfit model, and the performance improvement is real.\n\n#### Large\n\nBy now React has bloated to what, 100+ KiB minified? More? Fortunately, Preact solves that (≈10 KiB minified at the time of writing). Prax is even smaller; a few kilobytes unminified, with no dependencies.\n\n### Why not Svelte?\n\nSvelte has similar design goals, but seems to require a build system, which automatically invalidates it for me. With native module support in browsers, you can _run from source_. Don't squander this.\n\nPrax targets a particular breed of SSR+SPA apps, for which Svelte might be unfit. I haven't checked Svelte's server-rendering story. See the constraints outlined above.\n\n### Why not plain strings?\n\nFor the application architecture espoused by Prax, it would be even simpler and faster to use strings, so why bother with Prax?\n\n```js\n`\u003cdiv class=\"${cls}\"\u003e${content}\u003c/div\u003e`\n```\n\n* Prax provides an isomorphic API that renders to strings in Deno/Node, and to DOM nodes in browsers.\n* Prax handles a myriad of HTML/XML gotchas, such as content escaping, nil tolerance, and various bug prevention measures.\n* Prax is JSX-compatible, without any React gunk.\n* Nested function calls are more syntactically precise/rich/powerful than a large string. Editors provide better support. Syntax errors are immediately found.\n* Probably more.\n\n### Why not framework X?\n\nProbably never heard of X! For the very specific requirements outlined above, it was faster to build a fit, than to search among the thousands of unfits. If one already existed, let me know.\n\n## Usage\n\nWith NPM:\n\n```sh\nnpm i -E prax\n```\n\nWith URL imports in Deno:\n\n```js\nimport {E} from 'https://cdn.jsdelivr.net/npm/prax@0.8.0/str.mjs'\n```\n\nWith URL imports in browsers:\n\n```js\nimport {E} from 'https://cdn.jsdelivr.net/npm/prax@0.8.0/dom.mjs'\n```\n\nThis example uses plain JS. Prax is also [#compatible with JSX](#jsx). For a better experience, use native modules and run your app from source in both Deno/Node and browsers.\n\n```js\nimport {E, doc} from 'prax'\n\nfunction main() {\n  const html = Html({title: 'home', body: Index()})\n  console.log(html)\n}\n\nfunction Html({title, body}) {\n  return doc(\n    E('html', {lang: 'en'},\n      E('head', {},\n        E('link', {rel: 'stylesheet', href: '/styles/main.css'}),\n        !title ? null : E('title', {}, title),\n      ),\n      E('body', {},\n        body,\n        E('script', {type: 'module', src: '/scripts/browser.mjs'}),\n      ),\n    )\n  )\n}\n\nfunction Index() {\n  return E('div', {class: 'some-class'}, `Hello world!`)\n}\n```\n\nJSX example. See [#notes on JSX compatibility](#jsx).\n\n```js\n/* @jsx E */\n\nimport {E, doc} from 'prax'\n\nfunction main() {\n  const html = Html({title: 'home', body: Index()})\n\n  // In `str.mjs`, this is a string.\n  // In `dom.mjs`, this is a DOM tree.\n  console.log(html)\n}\n\nfunction Html({title, body}) {\n  return doc(\n    \u003chtml lang='en'\u003e\n      \u003chead\u003e\n        \u003clink rel='stylesheet' href='/styles/main.css' /\u003e\n        {!title ? null : \u003ctitle\u003e{title}\u003c/title\u003e}\n      \u003c/head\u003e\n      \u003cbody\u003e\n        {body}\n        \u003cscript type='module' src='/scripts/browser.mjs' /\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  )\n}\n\nfunction Index() {\n  return (\n    \u003cdiv class='some-class'\u003eHello world!\u003c/div\u003e\n  )\n}\n```\n\n## API\n\nPrax provides [#`dom.mjs`](#dommjs) for browsers and [#`str.mjs`](#strmjs) for Deno/Node. The rendering functions such as [#`E`](#etype-props-children) are somewhat isomorphic between these modules: you call them the same way, but the output is different, tailored to the environment. Its `package.json` specifies which file should be imported where, in ways understood by Node and bundlers such as Esbuild. You should be able to just:\n\n```js\nimport {E} from 'prax'\nimport * as x from 'prax'\n```\n\nAlso, you don't need a bundler! JS modules are natively supported by evergreen browsers. To avoid repeating the import URL, either list your dependencies in one module imported by the rest of the app, or use an importmap. Importmap support is [polyfillable](https://github.com/guybedford/es-module-shims). You can also use a bundler such as Esbuild _just_ for production builds.\n\n```html\n\u003cscript type=\"importmap\"\u003e\n  {\"imports\": {\"prax\": \"/node_modules/prax/dom.mjs\"}}\n\u003c/script\u003e\n```\n\nWhen using Deno, your importmap should choose [#`str.mjs`](#strmjs):\n\n```json\n{\"imports\": {\"prax\": \"/node_modules/prax/str.mjs\"}}\n```\n\n### Isomorphic\n\nThe following APIs are provided by both [#`dom.mjs`](#dommjs) and [#`str.mjs`](#strmjs).\n\n#### `E(type, props, ...children)`\n\nShort for \"element\", abbreviated for frequent use. Renders an HTML element. In [#`str.mjs`](#strmjs) returns a string, as `Raw` to indicate that it shouldn't be escaped. In [#`dom.mjs`](#dommjs) returns a DOM node.\n\n`type` must be a string. See [#Props](#props) for props rules, and [#Children](#children) for child rules.\n\n```js\nconst node = E('div', {class: 'one'}, 'two')\nconsole.log(node)\n\n// `str.mjs`: [String (Raw): '\u003cdiv class=\"one\"\u003etwo\u003c/div\u003e']\n// `dom.mjs`: \u003cdiv class=\"one\"\u003etwo\u003c/div\u003e\n```\n\nIn browsers, `props` is passed to `document.createElement` as-is, in order to support creation of [customized built-in elements](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#customized_built-in_element) via `is`. Also see [#Direct Instantiation](#direct-instantiation) for additional thoughts on this.\n\n```js\nclass Btn extends HTMLButtonElement {}\ncustomElements.define('a-btn', Btn, {extends: 'button'})\n\nimport {E} from 'prax'\n\n// Works in Deno/Node and browsers. Creates a `Btn` in browsers.\nE('button', {is: 'a-btn'})\n\n// Equivalent to the above, but works only in browsers.\nnew Btn()\n```\n\n#### `S(type, props, ...children)`\n\nExactly like [#`E`](#etype-props-children), but generates SVG markup. In Deno/Node, either function will work, but in browsers, you _must_ use `S` for SVG. It uses `document.createElementNS` with the SVG namespace.\n\nThis is because unlike every template-based system, including React, Prax renders _immediately_. Nested function calls are evaluated inner-to-outer. When rendering an arbitrary element like `path` (there are many!), `E` has no way of knowing that it will eventually be included into `svg`. HTML parsers automate this because they parse _outer_ elements first.\n\n```js\nimport {S} from 'prax'\n\nfunction SomeIcon() {\n  return S('svg', {class: 'my-icon'}, S('path', {}, '...'))\n}\n```\n\n#### `F(...children)`\n\nShort for \"fragment\". Renders the children without an enclosing element. In [#`str.mjs`](#strmjs), this simply combines their markup without any wrappers or delimiters, and returns a string as `Raw`. In [#`dom.mjs`](#dommjs), this returns a [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment).\n\nYou will rarely use this, because [#`E`](#etype-props-children) supports arrays of children, nested to any depth. `F` is used internally by [#`reset`](#resetelem-props-children).\n\n#### `cls(...vals)`\n\nCombines multiple CSS classes:\n\n* Ignores falsy values (nil, `''`, `false`, `0`, `NaN`).\n* Recursively traverses arrays.\n* Combines strings, space-separated.\n\n```js\nx.cls('one', ['two'], false, 0, null, [['three']])\n// 'one two three'\n```\n\n#### `len(children)`\n\nAnalog of `React.Children.count`. Counts non-nil children, recursively traversing arrays.\n\n```js\nconst children = ['one', null, [['two'], null]]\nx.len(children)\n// 2\n```\n\n#### `vac(children)`\n\nThe name is short for \"vacate\" / \"vacuum\" / \"vacuous\". Same as `len(children) ? children : undefined`, but more efficient.\n\n```js\nx.vac(null)\n// undefined\n\nx.vac([[[null]]])\n// undefined\n\nx.vac([null, 0, 'str'])\n// [null, 0, 'str']\n```\n\nThis function allows to use `\u0026\u0026` without accidentally rendering a falsy value such as `false` or `0`. Without `x.vac`, `\u0026\u0026` may evaluate to something not intended for rendering.\n\n```js\nx.vac(someValue) \u0026\u0026 E(`div`, {}, someValue)\n```\n\n#### `map(children, fun, ...args)`\n\nwhere `fun` is `ƒ(child, i, ...args)`\n\nAnalog of `React.Children.map`. Flatmaps `children` via `fun`, returning the resulting array. Ignores nils and recursively traverses arrays.\n\n```js\nconst children = ['one', null, [['two'], null]]\nfunction fun(...args) {return args}\nx.map(children, fun, 'bonus')\n// [['one', 0, 'bonus'], ['two', 1, 'bonus']]\n```\n\n#### `doc(val)`\n\nShortcut for prepending [`\u003c!doctype html\u003e`](https://developer.mozilla.org/en-US/docs/Glossary/Doctype).\n\n  * In [#`str.mjs`](#strmjs), this encodes `val` using the [#children](#children) rules, prepends doctype, and returns a plain string, which may be served over HTTP, written to a file, etc.\n  * In [#`dom.mjs`](#dommjs), this simply returns `val`. Provided for isomorphism.\n\n```js\nimport {E, doc} from 'prax'\n\nfunction onRequest(req, res) {\n  res.end(Html())\n}\n\nfunction Html() {\n  return doc(\n    E('html', {},\n      E('head'),\n      E('body'),\n    )\n  )\n}\n```\n\n#### `merge(...vals)`\n\nCombines multiple [#props](#props) into one, merging their `attributes`, `dataset`, `style`, `class`, `className` whenever possible. For other properties, this performs an override rather than merge (last value wins). In case of `style`, merging is done only for style dicts, not for style strings.\n\n```js\nimport * as x from 'prax'\n\nx.merge({class: `one`, onclick: someFunc}, {class: `two`, disabled: true})\n// {class: `one two`, onclick: someFunc, disabled: true}\n```\n\n#### `lax(val)`\n\nToggles lax/strict mode, which affects Prax's [#stringification rules](#stringable). This is a combined getter/setter:\n\n```js\nimport * as x from 'prax'\n\nx.lax()      // false\nx.lax(true)  // true\nx.lax()      // true\nx.lax(false) // false\nx.lax()      // false\n```\n\n#### `e(type, props, ...children)`\n\n(Better name pending.) Tiny shortcut for making shortcuts. Performs [partial application](https://en.wikipedia.org/wiki/Partial_application) of [#`E`](#etype-props-children) with the given arguments.\n\n```js\nexport const a    = e('a')\nexport const div  = e('div')\nexport const bold = e('strong', {class: 'weight-bold'})\n\nfunction Page() {\n  return div({},\n    a({href: '/'}, bold(`Home`)),\n  )\n}\n```\n\n### `dom.mjs`\n\n[dom.mjs](dom.mjs) should be used in browsers. Its rendering functions such as [#`E`](#etype-props-children) return actual DOM nodes. It also provides shortcuts for DOM mutation such as [#`reset`](#resetelem-props-children) for individual elements and [#`resetDoc`](#resetdochead-body) for the entire document.\n\n#### `reset(elem, props, ...children)`\n\nMutates the element, resetting it to the given props via [#`resetProps`](#resetpropselem-props) and replacing its children; see [#children rules](#children).\n\n`reset` carefully avoids destroying existing content on render exceptions. It buffers children in a temporary `DocumentFragment`, replacing the previous children only when fully built.\n\n```js\nimport * as x from 'prax'\n\nclass Btn extends HTMLButtonElement {\n  constructor() {\n    super()\n    this.onclick = this.onClick\n  }\n\n  onClick() {\n    x.reset(this, {class: 'activated', disabled: true}, `clicked!`)\n  }\n}\ncustomElements.define('a-btn', Btn, {extends: 'button'})\n```\n\n#### `resetProps(elem, props)`\n\nMutates the element, resetting its properties and attributes. Properties and attributes missing from `props` are not affected. To unset existing props, include them with the appropriate \"zero value\" (usually `null`/`undefined`).\n\nAvoids reassigning properties when values are identical via `Object.is`. Many DOM properties are setters with side effects. This avoids unexpected costs.\n\n```js\nimport * as x from 'prax'\n\nx.resetProps(elem, {class: 'new-class', hidden: false})\n```\n\n#### `replace(node, ...children)`\n\nShortcut for:\n\n```js\nimport * as x from 'prax'\n\nnode.parentNode.replaceChild(x.F(...children), node)\n```\n\n#### `resetDoc(head, body)`\n\nCarefully updates the current `document.head` and `document.body`. Shortcut for using [#`resetHead`](#resetheadhead) and [#`resetBody`](#resetbodybody) together. Example:\n\n```js\nimport * as x from 'prax'\nimport {E} from 'prax'\n\nfunction SomePage() {\n  x.resetDoc(\n    E(`head`, {},\n      E(`title`, {}, `some title`),\n      E(`meta`, {name: `author`, content: `some author`}),\n      E(`meta`, {name: `description`, content: `some description`}),\n    ),\n    E(`body`, {},\n      E(`p`, {}, `hello world!`),\n    )\n  )\n}\n```\n\n#### `resetHead(head)`\n\nTakes `HTMLHeadElement`, usually rendered with `E('head', ...)`, and carefully updates the current `document.head`. Rules:\n\n  * _Doesn't affect nodes that weren't previously passed to `resetHead`_.\n  * Instead of appending `\u003ctitle\u003e`, sets `document.title` to its text content.\n\nNodes previously passed to `resetHead` are tagged using a `WeakSet` which is exported under the name `metas` but undocumented. Adding other nodes to this set will cause Prax to replace them as well.\n\nSee [#`resetDoc`](#resetdochead-body) for examples.\n\n#### `resetBody(body)`\n\nTakes `HTMLBodyElement`, usually rendered with `E('body', ...)`, and replaces the current `document.body`, preserving focus if possible. See [#`resetDoc`](#resetdochead-body) for examples.\n\n#### `resetText(node, src)`\n\nTakes an `Element` and replaces its `textContent` by a stringified version of `src`, using Prax's [#stringification rules](#stringable). Returns the given element. Very similar to the following:\n\n```js\nnode.textContent = src\n```\n\n... but will _not_ render `[object Object]` or other similar garbage. See [#rules](#stringable).\n\n#### `props(node)`\n\nTakes an `Element` and returns _very approximate_ source props derived _only from attributes_.\n\n```js\nx.props(E('div', {class: 'one', dataset: {two: 'three'}}))\n// {dataset: DOMStringMap{two: \"three\"}, class: \"one\"}\n```\n\n### `str.mjs`\n\n[str.mjs](str.mjs) should be used in Deno/Node. Its rendering functions such as [#`E`](#etype-props-children) return strings, which can be sent to clients or written to disk.\n\nIt also exports some functions related to HTML encoding which are undocumented. Check the source if interested.\n\n### `reg.mjs`\n\n[reg.mjs](reg.mjs) provides shortcuts for registering custom DOM elements. It _can_ be imported in Deno/Node, but unlike the other modules, it's not meant for hybrid SSR/SPA. In hybrid rendering, custom element tags must be hardcoded, which requires extra code and is annoying without access to decorators. `reg.mjs` completely automates element registration by deriving tag names from class names, but should only be used in SPA that instantiate those classes with `new`.\n\n#### Base Classes\n\n[#`reg.mjs`](#regmjs) exports various base classes: `HTMLElement`, `HTMLAnchorElement`, and so on. All of them implement automatic registration on `new`, which also works for subclasses. When possible, they extend the corresponding native DOM classes, falling back on `HTMLElement`, falling back on `Object`.\n\nSubclassing these has two benefits:\n\n  * Automatic registration.\n  * Compatibility with Deno/Node.\n    * Without the DOM, modules can be executed/imported, but classes are nops.\n\n#### `reg(cls)`\n\nShort for \"register\". Registers a custom DOM element class. Automatically derives tag name from class name. Inspects the prototype chain to automatically determine the base tag for the `{extends}` option. Automatically avoids naming conflicts, but the resulting tag names are non-deterministic and should not be relied upon.\n\nThis function is idempotent: repeated calls with the same class are nops. This is handy for `new.target`, see below. All of the following examples are nearly equivalent.\n\n```js\nimport * as r from 'prax/reg.mjs'\n\n// Recommended approach. Automatically registers on `new`.\n{\n  class Link extends r.HTMLAnchorElement {}\n}\n\n{\n  class Link extends HTMLAnchorElement {}\n  r.reg(Link)\n}\n\n{\n  @r.reg\n  class Link extends HTMLAnchorElement {}\n}\n\n{\n  class Link extends HTMLAnchorElement {\n    constructor() {\n      r.reg(new.target)\n      super()\n    }\n  }\n}\n\n// Built-in approach. Annoying to use.\n{\n  class Link extends HTMLAnchorElement {}\n  customElements.define(`a-link`, Link, {extends: `a`})\n}\n```\n\n### `rcompat.mjs`\n\n[rcompat.mjs](rcompat.mjs) exports a few functions for JSX compatibility and migrating code from React. See the section [#JSX](#jsx) for usage examples. Important exports:\n\n* `R`\n* `F` (different one!)\n\n### Undocumented\n\nSome tools are exported but undocumented to avoid bloating the docs. The source code should be self-explanatory.\n\n## Imperative, Synchronous\n\nImperative control flow and immediate, synchronous side effects are precious things. Don't squander them carelessly.\n\nIn Prax, everything is immediate. Rendering exceptions can be caught via `try/catch`. Magic context can be setup trivially in user code, via `try/finally`, without library support. Lifecycle stages such as \"before render\" and \"after DOM mounting\" can be done just by placing lines of code before and after a [#`reset`](#resetelem-props-children) call. (Also via native lifecycle callbacks in custom elements, which doesn't require library support.)\n\nCompare the hacks and workarounds invented in React to implement the same trivial things.\n\n## Direct Instantiation\n\nUnlike most \"modern\" rendering libraries, Prax doesn't stand between you and DOM elements. Functions such as [#`E`](#etype-props-children) are trivial shortcuts for `document.createElement`. This has nice knock-on effects for [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements).\n\nIsomorphic code that runs in Deno/Node and browsers must use `E`, because on the server you always render to a string. However, code that runs _only_ in the browser is free to use direct instantiation, with custom constructor signatures.\n\nThe following example is a trivial custom element that takes an observable object and displays one of its properties as text. Subscription and unsubscription is automatic. (Observable signature approximated from [Espo → `isObs`](https://github.com/mitranim/espo).)\n\nYou can simply `new RecText(obs)`, passing a specific observable. No \"props\" involved in this. No weird library gotchas to deal with. When using TS or Flow, signatures can be properly typed, without hacks and workarounds such as \"prop types\".\n\n```js\nimport {E, reset} from 'prax'\n\nfunction SomeMarkup() {\n  return new RecText(someObservable, {class: 'text'})\n}\n\nclass RecText extends HTMLElement {\n  constructor(observable, props) {\n    super()\n    this.obs = observable\n    x.resetProps(this, props)\n  }\n\n  connectedCallback() {\n    this.obs.sub(this)\n    this.trig()\n  }\n\n  disconnectedCallback() {\n    this.obs.unsub(this)\n  }\n\n  trig() {\n    this.textContent = this.obs.val\n  }\n}\ncustomElements.define('a-rec-text', RecText)\n```\n\n## Props\n\nJust like React, Prax conflates attributes and properties, calling everything \"props\". Here are the rules.\n\n* The term `nil` stands for both `null` and `undefined`.\n* Props as a whole are `nil | {}`.\n* Any prop with a `nil` value is either unset or skipped, as appropriate.\n* [`class`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class) and [`className`](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) are both supported, as `nil | string`.\n* [`attributes`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) is `nil | {}`. Every key-value is assumed to be an attribute, even in browsers, and follows the normal attribute assignment rules; see below.\n* [`style`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style) is `nil | string | {}`. If `{}`, it must have `camelCase` keys, matching the structure of a [`CSSStyleDeclaration`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) object. Values are `nil | string`.\n* [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/dataset) must be `nil | {}`, where keys are `camelCase` without the `data-` prefix. Values follow the attribute encoding rules. In [#`str.mjs`](#strmjs), `dataset` is converted to [`data-*`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes. You can also just use those attributes.\n* [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) works in both environments, and must be `nil | string`.\n* [`for`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for) and [`htmlFor`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor) both work, and must be `nil | string`.\n* ARIA properties such as [`ariaCurrent`](https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaCurrent) work in both environments, and must be `nil | string`. In [#`str.mjs`](#strmjs), they're converted to kebab-cased `aria-*` attributes. You can use both property names and attribute names.\n* The value of any attribute, or a DOM property whose type is known to be `string`, must be either `nil` or [#stringable](#stringable).\n\nAdditional environment-specific rules:\n\n* In [#`str.mjs`](#strmjs), everything non-special-cased is assumed to be an attribute, and must be `nil` or [#stringable](#stringable).\n* In [#`dom.mjs`](#dommjs), there's a heuristic for deciding whether to assign a property or attribute. Prax will try to default to properties, but use attributes as a fallback for properties that are completely unknown or whose value doesn't match the type expected by the DOM.\n\nUnlike React, Prax has _no made-up properties_ or weird renamings. Use `autocomplete` rather than `autoComplete`, `oninput` rather than `onChange`, and so on.\n\n## Children\n\nAll rendering functions, such as [#`E`](#etype-props-children) or [#`reset`](#resetelem-props-children), take `...children` as [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) and follow the same rules:\n\n* Nil (`null` or `undefined`) is ignored.\n* `''` is also ignored (doesn't create a `Text` node).\n* `[]` is traversed, recursively if needed. The following are all equivalent: `a, b, c` | `[a, b, c]` | `[[a], [[[b, [[c]]]]]]`.\n* As a consequence of the previous rules, `[null, [undefined]]` is the same as no children at all.\n* Other primitives (numbers, bools, symbols) are stringified.\n* `new Raw` strings are considered \"inner HTML\". In [#`str.mjs`](#strmjs), their content is included verbatim and not escaped. In [#`dom.mjs`](#dommjs), their content is parsed into DOM nodes, similarly to `innerHTML`.\n* Anything else must be a [#stringable object](#stringable).\n\nIn [#`str.mjs`](#strmjs), _after_ resolving all these rules, the output string is escaped, following [standard rules](https://www.w3.org/TR/html52/syntax.html#escaping-a-string). The only exception is `new Raw` strings, which are verbatim.\n\nCaution: literal content of `script` elements may require additional escaping when it contains `\u003c/script\u003e` inside strings, regexps, and so on. The following example generates broken markup, and will display a visible `')`. Prax currently doesn't escape this automatically.\n\n```js\nE('script', {}, new Raw(`console.log('\u003c/script\u003e')`))\n```\n\n```html\n\u003cscript\u003econsole.log('\u003c/script\u003e')\u003c/script\u003e\n```\n\n## Stringable\n\nPrax's stringification rules are carefully designed to minimize gotchas and bugs.\n\n* `null` and `undefined` are equivalent to `''`.\n* Other primitives are stringified. (For example, `0` → `'0'`, `false` → `'false'`, `NaN` → `'NaN'`.)\n* Objects without a custom `.toString` method are verboten. This includes `{}` and a variety of other classes. This is a bug prevention measure: the vast majority of such objects are never intended for rendering, and are only passed accidentally.\n  * When `lax(false)` (default, recommended for development), non-stringable objects cause exceptions.\n  * When `lax(true)` (recommended for production), non-stringable objects are treated as nil.\n  * See the [#`lax`](#laxval) function.\n* Other objects are stringified via their `.toString`. For example, rendering a `Date` or `URL` object is OK.\n\n## JSX\n\nPrax comes with an optional adapter for JSX compatibility and migrating React-based code. Requires a bit of wiring-up. Make a file with the following:\n\n```js\nimport * as x from 'prax'\nimport {R} from 'prax/rcompat.mjs'\n\nexport {F} from 'prax/rcompat.mjs'\nexport function E(...args) {return R(x.E, ...args)}\nexport function S(...args) {return R(x.S, ...args)}\n```\n\nConfigure your transpiler (Babel / Typescript / etc.) to use the \"legacy\" JSX transform, calling the resulting `E` for normal elements and `F` for fragments.\n\nUnlike React, Prax can't use the same function for normal and SVG elements. Put all your SVG into a separate file, with a JSX pragma to use this special `S` function for that file.\n\nAfterwards, the following should work:\n\n```jsx\nfunction Outer() {\n  return \u003cInner class='one'\u003etwo\u003c/Inner\u003e\n}\n\nfunction Inner({children, ...props}) {\n  return \u003cdiv {...props}\u003eone {children} two\u003c/div\u003e\n}\n```\n\n## License\n\nhttps://unlicense.org\n\n## Misc\n\nI'm receptive to suggestions. If this library _almost_ satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fprax","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmitranim%2Fprax","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fprax/lists"}