{"id":30269833,"url":"https://github.com/ryupold/vode","last_synced_at":"2026-04-18T02:21:22.633Z","repository":{"id":304713218,"uuid":"630607588","full_name":"ryupold/vode","owner":"ryupold","description":"A compact web framework for minimalist developers. Zero dependencies, no build step except for typescript compilation, and a simple virtual DOM implementation that is easy to understand and use.","archived":false,"fork":false,"pushed_at":"2026-04-16T23:24:09.000Z","size":792,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-16T23:31:44.082Z","etag":null,"topics":["ai-friendly","framework","frontend","library","memoization","minimal","no-build","rendering","simple","state","state-management","typescript","web"],"latest_commit_sha":null,"homepage":"https://ryupold.de/post/vode","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryupold.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-04-20T18:44:08.000Z","updated_at":"2026-04-16T23:24:11.000Z","dependencies_parsed_at":"2025-07-14T23:20:07.703Z","dependency_job_id":"5e78c33b-c966-4e12-bbf0-70abb94ee8bd","html_url":"https://github.com/ryupold/vode","commit_stats":null,"previous_names":["ryupold/vode"],"tags_count":50,"template":false,"template_full_name":null,"purl":"pkg:github/ryupold/vode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryupold%2Fvode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryupold%2Fvode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryupold%2Fvode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryupold%2Fvode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryupold","download_url":"https://codeload.github.com/ryupold/vode/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryupold%2Fvode/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31953567,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"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":["ai-friendly","framework","frontend","library","memoization","minimal","no-build","rendering","simple","state","state-management","typescript","web"],"created_at":"2025-08-16T02:19:21.544Z","updated_at":"2026-04-18T02:21:22.601Z","avatar_url":"https://github.com/ryupold.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ![vode-logo](https://raw.githubusercontent.com/ryupold/vode/refs/heads/main/logo.webp)\n| [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue?logo=typescript)](https://www.typescriptlang.org/) |  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)   |\n| :-------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------: |\n|         [![NPM](https://badge.fury.io/js/%40ryupold%2Fvode.svg)](https://www.npmjs.com/package/@ryupold/vode)         | [![Dependencies](https://img.shields.io/badge/dependencies-0-success)](package.json) |\n\n---\n\nA compact web framework for minimalist developers. Zero dependencies, no build step except for typescript compilation, and a simple virtual DOM implementation that is easy to understand and use. Autocompletion out of the box thanks to `lib.dom.d.ts`.\n\nIt brings a primitive building block to the table that gives flexibility in composition and makes refactoring easy. \nThe use cases can be single page applications or isolated components with complex state.\n\n## Usage\n\n### ESM\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003ctitle\u003eVode ESM Example\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\"\u003e\n        import { app, BR, DIV, INPUT, SPAN } from 'https://unpkg.com/@ryupold/vode/dist/vode.min.mjs';\n\n        const appNode = document.getElementById('app');\n\n        const state = { counter: 0 };\n\n        app(appNode, state,\n            (s) =\u003e [DIV,\n                [INPUT, {\n                    type: 'button',\n                    onclick: { counter: s.counter + 1 },\n                    value: 'Click me',\n                }],\n                [BR],\n                [SPAN, { style: { color: 'red' } }, `${s.counter}`],\n            ]\n        );\n    \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Classic (iife)\nBinds the library to the global `V` variable.\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cscript src=\"https://unpkg.com/@ryupold/vode/dist/vode.es5.min.js\"\u003e\u003c/script\u003e\n    \u003ctitle\u003eVode ES5 (iife) Script Example\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\u003c/div\u003e\n    \u003cscript\u003e\n        var appNode = document.getElementById('app');\n\n        var state = { counter: 0 };\n\n        V.app(appNode, state,\n            function (s) {\n                return [\"div\",\n                    [\"input\", {\n                        type: 'button',\n                        onclick: { counter: s.counter + 1 },\n                        value: 'Click me',\n                    }\n                    ],\n                    [\"br\"],\n                    [\"span\", { style: { color: 'red' } }, '' + s.counter]\n                ]\n            });\n    \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### NPM\n\n[![NPM](https://nodei.co/npm/@ryupold/vode.svg?color=red\u0026data=n,v,s,d,u)](https://www.npmjs.com/package/@ryupold/vode)\n\nindex.html\n\n```html\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003ctitle\u003eVode NPM Example\u003c/title\u003e\n    \u003cscript type=\"module\" src=\"main.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\u003c/div\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nmain.ts\n```typescript\nimport { app, createState, BR, DIV, INPUT, SPAN } from '@ryupold/vode';\n\nconst state = createState({\n    counter: 0,\n});\n\ntype State = typeof state;\n\nconst appNode = document.getElementById('app')!;\n\napp\u003cState\u003e(appNode, state,\n    (s: State) =\u003e [DIV,\n        [INPUT, {\n            type: 'button',\n            onclick: { counter: s.counter + 1 },\n            value: 'Click me',\n        }],\n        [BR],\n        [SPAN, { style: { color: 'red' } }, `${s.counter}`],\n    ]\n);\n```\n\n## `[V,{},d,e]`\n\nLets describe UI as data structures that map 1:1 to DOM elements.\n\nA `vode` is a representation of a virtual DOM node, which is a tree structure of HTML elements. It is written as tuple:\n\n```\n[TAG, PROPS?, CHILDREN...]\n```\n\nAs you can see, it is a simple array with the first element being the tag name, the second element being an optional properties object, and the rest being child-vodes.\n\nThey are lightweight structures to describe what the DOM should look like.\n\nImagine this HTML:\n\n```html\n\u003cdiv class=\"card\"\u003e\n  \u003cdiv class=\"card-image\"\u003e\n    \u003cfigure class=\"image is-4by3\"\u003e\n      \u003cimg\n        src=\"placeholders/1280x960.png\"\n        alt=\"Placeholder image\"\n      /\u003e\n    \u003c/figure\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"card-content\"\u003e\n    \u003cdiv class=\"media\"\u003e\n      \u003cdiv class=\"media-left\"\u003e\n        \u003cfigure class=\"image is-48x48\"\u003e\n          \u003cimg\n            src=\"placeholders/96x96.png\"\n            alt=\"Placeholder image\"\n          /\u003e\n        \u003c/figure\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"media-content\"\u003e\n        \u003cp class=\"title is-4\"\u003eJohn Smith\u003c/p\u003e\n        \u003cp class=\"subtitle is-6\"\u003e@johnsmith\u003c/p\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n\n    \u003cdiv class=\"content\"\u003e\n      Lorem ipsum dolor sit amet, consectetur adipiscing elit. \n      \u003ca href=\"?post=vode\"\u003evode\u003c/a\u003e. \u003ca href=\"#\"\u003e#css\u003c/a\u003e\n      \u003ca href=\"#\"\u003e#responsive\u003c/a\u003e\n      \u003cbr /\u003e\n      \u003ctime datetime=\"2025-09-24\"\u003e10:09 PM - 24 Sep 2025\u003c/time\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nexpressed as *vode* it would look like this:\n\n```typescript\n[DIV, { class: 'card' },\n    [DIV, { class: 'card-image' },\n        [FIGURE, { class: 'image is-4by3' },\n            [IMG, {\n                src: 'placeholders/1280x960.png',\n                alt: 'Placeholder image'\n            }]\n        ]\n    ],\n    [DIV, { class: 'card-content' },\n        [DIV, { class: 'media' },\n            [DIV, { class: 'media-left' },\n                [FIGURE, { class: 'image is-48x48' },\n                    [IMG, {\n                        src: 'placeholders/96x96.png',\n                        alt: 'Placeholder image'\n                    }]\n                ]\n            ],\n            [DIV, { class: 'media-content' },\n                [P, { class: 'title is-4' }, 'John Smith'],\n                [P, { class: 'subtitle is-6' }, '@johnsmith']\n            ]\n        ],\n        [DIV, { class: 'content' },\n            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ',\n            [A, {href: '?post=vode'}, 'vode'], '. ', [A, { href: '#' }, '#css'],\n            [A, { href: '#' }, '#responsive'],\n            [BR],\n            [TIME, { datetime: '2025-09-24' }, '10:09 PM - 24 Sep 2025']\n        ]\n    ]\n]\n```\n\nViewed in isolation, it does not provide an obvious benefit (apart from looking better imho), \nbut as the result of a function of state, it can become very useful to express conditional UI this way. \n\n### component\n\n```typescript\ntype Component\u003cS\u003e = (s: S) =\u003e ChildVode\u003cS\u003e;\n```\n\nA `Component\u003cState\u003e` is a function that takes a state object and returns a `Vode\u003cState\u003e` or `string`. \nIt is used to render the UI based on the current state. \nA new *vode* must be created on each render, otherwise it would be skipped which could lead to unexpected results. If you seek to improve render performance, have a look at the [`memo`](#memoization) function.\n\n```typescript\n// A full vode has a tag, properties, and children. props and children are optional.\nconst CompFoo = (s) =\u003e [SPAN, { class: \"foo\" }, s.isAuthenticated ? \"foo\" : \"bar\"];\n\nconst CompBar = (s) =\u003e [DIV, { class: \"container\" }, \n    \n    // a child vode can be a string, which results in a text node\n    [H1, \"Hello World\"], \n    \n    // a vode can also be a self-closing tag\n    [HR],\n\n    // conditional rendering\n    s.isAuthenticated \n        ? [STRONG, `and also hello ${s.user}`]\n        : [FORM,\n            [INPUT, { type: \"email\", name: \"email\" }],\n            [INPUT, { type: \"password\", name: \"pw\" }],\n            [INPUT, { type: \"submit\" }],\n        ],\n    // a child-vode of false, undefined or null is not rendered \n    !s.isAuthenticated \u0026\u0026 [HR],\n    \n    // style object maps directly to the HTML style attribute\n    [P, { style: { color: \"red\", fontWeight: \"bold\" } }, \"This is a paragraph.\"],\n    [P, { style: \"color: red; font-weight: bold;\" }, \"This is also a paragraph.\"],\n\n    // class property has multiple forms\n    [UL,\n        [LI, {class: \"class1 class2\"}, \"as string\"],\n        [LI, {class: [\"class1\", \"class2\"]}, \"as array\"],\n        [LI, {class: {class1: true, class2: false}}, \"as Record\u003cstring, boolean\u003e\"],\n    ],\n\n    // events get the state object as first argument\n    // and the HTML event object as second argument\n    [BUTTON, {\n        // all on* events accept `Patch\u003cState\u003e`\n        onclick: (s, evt) =\u003e {\n            // objects returned by events are patched automatically\n            return { counter: s.counter + 1 }; \n        },\n\n        // you can set the patch object directly for events\n        onmouseenter: { pointing: true },\n        onmouseleave: { pointing: false },\n\n        // a patch can be an async function\n        onmouseup: async (s, evt) =\u003e {\n            s.patch({ loading: true });\n            const result = await apiCall();\n            return { title: result.data.title, loading: false };\n        },\n\n        // you can also use a generator function that yields patches\n        onmousedown: async function* (s, evt) {\n            yield { loading: true }; \n            const result = await apiCall();\n            yield { \n                body: result.data.body,\n            };\n            return { loading: false };\n        },\n\n        // events can be attached conditionally\n        ondblclick : s.counter \u003e 20 \u0026\u0026 (s, evt) =\u003e {\n            return { counter: s.counter * 2 }; \n        },\n\n        class: { bar: s.pointing }\n    }, \"Click me!\"],\n\n    // components can be used as child-vodes, they are called lazy on render\n    CompFoo,\n    // or this way\n    CompFoo(s),\n];\n```\n\n### app\n\n`app` is a function that takes a HTML node, a state object, and a render function (`Component\u003cState\u003e`).  \n\n```typescript\nconst containerNode = document.getElementById('ANY-ELEMENT');\nconst state = {\n    counter: 0,\n    pointing: false,\n    loading: false,\n    title: '',\n    body: '',\n};\n\nconst patch = app(\n    containerNode, \n    state, \n    (s) =\u003e \n        [DIV, \n            [P, { style: { color: 'red' } }, `${s.counter}`],\n            [BUTTON, { onclick: () =\u003e ({ counter: s.counter + 1 }) }, 'Click me'],    \n        ]\n    );\n```\n\nIt will analyze the current structure of the given `ContainerNode` and adjust its structure in the first render. \nWhen render-patches are applied to the `patch` function or via yield/return of events, \nthe `ContainerNode` is updated to match the vode structure 1:1. \n\n#### defuse\n\nTo release resources associated with the vode app instance, you can call the `defuse` function on the `ContainerNode` that was passed to `app`.\n\n```typescript\nimport { app, defuse } from '@ryupold/vode';\nconst containerNode = document.getElementById('ANY-ELEMENT');\nconst state = { /* ... */ };\napp(containerNode, state, s =\u003e /* ... */ );\n//... later ...\n// when you want to clean up the vode app instance\ndefuse(containerNode);\n```\n\nThe DOM elements created by the vode app will remain in the `ContainerNode`, but all event listeners and references to the state object will be removed, allowing for proper garbage collection.\n\n### state \u0026 patch\nThe state object you pass to [`app`](#app) can be updated directly or via `patch`. \nDuring the call to `app`, the state object is bound to the vode app instance and becomes a singleton from its perspective. \nAlso a `patch` function is added to the state object; it is the same function that is also returned by `app`.\nA re-render happens when a patch object is supplied to the `patch` function or via event.\nWhen an object is passed to `patch`, its properties are recursively deep merged onto the state object.\n\n```javascript\nconst s = {\n    counter: 0,\n    pointing: false,\n    loading: false,\n    title: 'foo',\n    body: '',\n};\n\napp(appNode, s, s =\u003e AppView(s)); \n// after calling app(), the state object is bound to the appNode\n\n// update state directly as it is a singleton (silent patch, no render)\ns.title = 'Hello World';\n\n// render patch\ns.patch({});\n\n// render patch with a change that is applied to the state \ns.patch({ title: 'bar' }); \n\n// patch with a function that receives the state\ns.patch((s) =\u003e ({body: s.body + ' baz'})); \n\n// patch with an async function that receives the state\ns.patch(async (s) =\u003e {\n    s.loading = true; // sometimes it is easier to combine a silent patch\n    s.patch({});      // with an empty render patch\n    const result = await apiCall();\n    return { title: result.title, body: result.body, loading: false };\n}); \n\n// patch with an async generator function that yields patches\ns.patch(async function*(s){\n    yield { loading: true };\n    const result = await apiCall();\n    yield { title: result.title, body: result.body };\n    return { loading: false }; \n});\n\n// ignored, also: undefined, number, string, boolean, void\ns.patch(null);\n\n// setting a property in a patch to undefined deletes it from the state object\ns.patch({ pointing: undefined });\n\n// ❌ it is discouraged to patch inside the render step 💩\nconst ComponentEwww = (s) =\u003e {\n    if(!s.isLoading)\n        s.patch(() =\u003e startLoading());\n\n    return [DIV, s.isLoading ? [PROGRESS] : s.title];\n}\n\n// ✨ experimental view transitions support ✨\n// patch with a render via view transition\ns.patch([{}, (s) =\u003e {/*...*/}]); //all given patches will be part of a view transition\n\n// empty array patches command to skip the current view transition\n// and set the queued animated patches until now as current state with a sync patch\ns.patch([]);\n\n// skip current view transition and start this view transition instead\ns.patch([[], { loading: true }]);\n```\n\n### memoization\nTo optimize performance, you can use `memo(depsArray, Component | PropsFactory)` to cache the result of a component function. If the array of dependencies does not change (shallow compare), the component function is not called again, indicating for the render to skip this node and all its children.\nThis is useful when the creation of the vode is expensive or the rendering of it takes a significant amount of time.\n\n\n```typescript\nconst CompMemoList = (s) =\u003e \n    [DIV, { class: \"container\" }, \n        [H1, \"Hello World\"], \n        [BR], \n        [P, \"This is a paragraph.\"],\n        \n        // expensive component to render\n        memo(\n            // dependency array is shallow compared (using === operator) to the previous renders' memo dependencies\n            [s.title, s.body], \n            // this is the component function that will be \n            // called only when the array changes\n            (s) =\u003e {\n                const list = [UL];\n                for (let i = 0; i \u003c 1000; i++) {\n                    list.push([LI, `Item ${i}`]);\n                }\n                return list;\n            },\n        )\n    ];\n```\nPassing an empty dependency array means the component is only rendered once and then ignored.\n\nYou can also pass a function that returns the Props object to memoize the attributes.\n\n```typescript\nconst CompMemoProps = (s) =\u003e [DIV, \n    memo([s.isActive], (s) =\u003e ({ \n        class: s.isActive ? 'active' : 'inactive' \n    })),\n    \"Content\"\n];\n```\n\n### error handling\n\nYou can catch errors during rendering by providing a `catch` property in the vode props.\n\n```typescript\nconst CompWithError: ChildVode = () =\u003e\n    [DIV,\n        {\n            catch: (s: unknown, err: any) =\u003e [SPAN, { style: { color: 'red' } }, `An error occurred: ${err?.message}`],\n        },\n\n        [P, \"Below error is intentional for testing error boundaries:\"],\n\n        [DIV, {\n            // catch: [SPAN, { style: { color: 'red' } }, `An error occurred!`], // uncomment to catch child error directly here\n            onMount: () =\u003e {\n                throw new Error(\"Test error boundary in post view....\");\n            }\n        }],\n    ];\n```\n\nIf the `catch` property is a function, it will be called with the current state and the error as arguments, and should return a valid child-vode to render instead.\nIf it is a vode, it will be rendered directly. \nIf no `catch` property is provided, the error will propagate to the nearest ancestor that has a `catch` property defined, or to the top-level app if none is found.\nTry to keep the `catch` blocks as specific as possible to avoid masking other errors. \nOr just don't make errors happen in the first place :)\n\n### helper functions\n\nThe library provides some helper functions for common tasks.\n\n```typescript\nimport { tag, props, children, mergeClass, hydrate, vode } from '@ryupold/vode';\n\n// Merge class props intelligently (additive)\nmergeClass('foo', ['baz', 'bar']);  // -\u003e 'foo baz bar'\nmergeClass(['foo'], { bar: true, baz: false }); // -\u003e 'foo bar'\nmergeClass({zig: true, zag: false}, 'foo', ['baz', 'bar']);  // -\u003e 'zig foo baz bar'\n\n// Merge style props intelligently (same style properties are overwritten from left to right)\nmergeStyle({ color: 'red' }, 'font-weight: bold;'); // -\u003e 'color: red; font-weight: bold;'\nmergeStyle('color: white; background-color: blue;', { marginTop: '10px', color: 'green' }); // -\u003e 'background-color: blue; margin-top: 10px; color: green;'\n\n// Merge props objects intelligently (class and style props are merged with the helper functions above, other props are overwritten from left to right)\nmergeProps(\n    { title: 'Hello', src: 'foo.png', class: 'foo', style: { color: 'red' } },\n    { id: 'my-element', src: 'bar.png', class: ['bar', 'baz'], style: 'font-weight: bold;' },\n); \n/* -\u003e { \n  title: 'Hello', \n  id: 'my-element', \n  src: 'bar.png', \n  class: 'foo bar baz', \n  style: 'color: red; font-weight: bold;' \n} */\n\n// create a vode\nconst myVode: Vode = [DIV, { class: 'foo' }, [SPAN, 'hello'], [STRONG, 'world']];\nconst alsoMyVode1: Vode = vode(DIV, { class: 'foo' }, [SPAN, 'hello'], [STRONG, 'world']);\nconst alsoMyVode2: Vode = vode([DIV, { class: 'foo' }, [SPAN, 'hello'], [STRONG, 'world']]);\n\n// access parts of a vode\ntag(myVode);        // 'div'\nprops(myVode);      // { class: 'foo' }\nchildren(myVode);   // [[SPAN, 'hello'], [STRONG, 'world']]\n\n// get existing DOM element as a vode (can be helpful for analyzing/debugging)\nconst asVode = hydrate(document.getElementById('my-element'));\n```\n\nAdditionally to the standard HTML attributes, you can define 2 special event attributes: \n`onMount(State, Element)` and `onUnmount(State, Element)` in the vode props. \nThese are called when the element is created or removed during rendering. \nThey receive the `State` as the first argument and the DOM element as the second argument.\nLike the other events they can be patches too. \n\u003e Be aware that `onMount/onUnmount` are only called when an element \n\u003e is actually created/removed which might not always be the case during \n\u003e rendering, as only a diff of the virtual DOM is applied.\n\n### SVG \u0026 MathML\nSVG and MathML elements are supported but need the namespace defined in properties.\n\n```typescript\nimport { SVG, CIRCLE } from '@ryupold/vode';\n\nconst CompSVG = (s) =\u003e \n    [SVG, { xmlns: 'http://www.w3.org/2000/svg', width: 100, height: 100 },\n        [CIRCLE, { cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow' }]\n    ];\n```\n\n```typescript\nimport { MATH, MSUP, MI, MN } from '@ryupold/vode';\n\nconst CompMathML = (s) =\u003e \n    [MATH, { xmlns: 'http://www.w3.org/1998/Math/MathML' },\n        [MSUP, \n            [MI, 'x'], \n            [MN, '2']\n        ]\n    ];\n```\n\n### advanced usage\n\n#### state context\n\nThe state context utilities can help creating shareable type safe components.\n\n```typescript\nimport { app, context, createState, SubContext, Vode, DIV, FORM, H1, OPTION, P, SELECT } from \"@ryupold/vode\";\n\ntype Settings = { theme: string, lang: string };\ntype StateType = {\n    user: {\n        profile: { settings: Settings }\n    }\n};\n\nconst state = createState\u003cStateType\u003e({\n    user: {\n        profile: {\n            settings: { theme: 'dark', lang: 'es' }\n        }\n    }\n});\n\n// Create a context for the nested settings\nconst settingsCtx = context(state).user.profile.settings;\n\nconst element = document.getElementById('app')!;\napp(element, state,\n    (s) =\u003e [DIV,\n        [H1, \"Settings\"],\n        SettingsForm(settingsCtx),\n    ]\n);\n\nfunction SettingsForm(ctx: SubContext\u003cSettings\u003e) {\n    const settings = ctx.get()!; // { theme: 'dark', lang: 'es' }\n\n    return \u003cVode\u003e[FORM,\n        [P, \"current theme:\", settings.theme],\n        [SELECT,\n            {\n                class: 'theme-select',\n                onchange: (s: unknown, e: Event) =\u003e ctx.patch({ theme: (\u003cHTMLSelectElement\u003ee.target).value }),\n                value: settings.theme,\n            },\n            [OPTION, { value: 'light', selected: settings.theme === 'light' }, 'light'],\n            [OPTION, { value: 'dark', selected: settings.theme === 'dark' }, 'dark'],\n        ],\n        [P, \"current lang:\", settings.lang],\n        [SELECT, {\n            class: 'lang-select',\n            onchange: (s: unknown, e: Event) =\u003e ctx.patch({ lang: (\u003cHTMLSelectElement\u003ee.target).value }),\n            value: settings.lang,\n        },\n            [OPTION, { value: 'en', selected: settings.lang === 'en' }, 'en'],\n            [OPTION, { value: 'de', selected: settings.lang === 'de' }, 'de'],\n            [OPTION, { value: 'es', selected: settings.lang === 'es' }, 'es'],\n            [OPTION, { value: 'fr', selected: settings.lang === 'fr' }, 'fr'],\n        ],\n    ];\n}\n```\n\n#### isolated state\nYou can have multiple isolated vode app instances on a page, each with its own state and render function.\nThe returned patch function from `app` can be used to synchronize the state between them.\n\n#### nested vode-app\nIt is possible to nest vode-apps inside vode-apps, but the library is not opinionated on how you do that. \nOne can imagine this type of component:\n\n```typescript\nexport function IsolatedVodeApp\u003cOuterState, InnerState\u003e(\n    tag: Tag,\n    state: InnerState,\n    View: (ins: InnerState) =\u003e Vode\u003cInnerState\u003e,\n): ChildVode\u003cOuterState\u003e {\n    return memo\u003cOuterState\u003e([],\n        () =\u003e [tag,\n            {\n                onMount: (s: OuterState, container: Element) =\u003e {\n                    app\u003cInnerState\u003e(container, state, View);\n                }\n            }\n        ]\n    );\n}\n```\n\nThe memo with empty dependency array prevents further render calls from the outer app\nso rendering of the subtree inside is controlled by the inner app.\nTake note of the fact that the top-level element of the inner app refers to the surrounding element and will change its state accordingly.\n\n#### view transitions\nThe library has experimental support for [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API).\nYou can pass an array of patches to the `patch` function where each patch will be applied with the next available view transition.\n\nPatching an empty array `[]` will skip the current view transition and set the queued animated patches until now as current state with a sync patch. \n\u003e Keep in mind that view transitions are not supported in all browsers yet and only one active transition can happen at a time. This feature may change significantly in the future, so do not rely on it heavily.\n\nScheduling behaviour can in theory be overridden with `containerNode._vode.asyncRenderer`.\n\n```javascript\n// or globally disable view transitions for the vode framework\nimport { globals } from '@ryupold/vode';\nglobals.startViewTransition = null;\n\n// set to disable view transitions for specific vode-app\ncontainerNode._vode.asyncRenderer = null;\n```\n\n### performance\n\nThere are some metrics available on the appNode. \nThey are updated on each render.\n\n```typescript\napp\u003cState\u003e(appNode, state, (s) =\u003e ...);\n\nconsole.log(appNode._vode.stats);\n```\n\n```javascript\n{\n    // number of patches applied to the state overall\n    patchCount: 100,\n    // number of render-patches (objects) overall\n    syncRenderPatchCount: 55,\n    // number of view transition render-patches (arrays) overall\n    asyncRenderPatchCount: 3,\n    // number of sync \"normal\" renders performed overall\n    syncRenderCount: 43,\n    // number of async renders performed overall\n    asyncRenderCount: 2,\n    // time the last render took in milliseconds\n    lastSyncRenderTime: 2,\n    // time the last view transition took in milliseconds\n    lastAsyncRenderTime: 21,\n    // number of active async running effects (function based patches)\n    liveEffectCount: 0,\n}\n```\n\nThe library is optimized for small to medium sized applications. In my own tests it could easily handle sites with tens of thousands of elements. Smart usage of `memo` can help to optimize performance further. You can find a comparison of the performance with other libraries [here](https://krausest.github.io/js-framework-benchmark/current.html).\n\nThis being said, the library does not focus on performance.\nIt is designed to feel nice while coding, by providing a primitive that is simple to bend \u0026 form.\nI want the mental model to be easy to grasp and the API surface to be small \nso that a developer can focus on building a web application instead of learning the framework and get to a flow state as quick as possible.\n\n## Thanks\n\nThe simplicity of [hyperapp](https://github.com/jorgebucaran/hyperapp) demonstrated that powerful frameworks don't require complexity, which inspired this library's design philosophy.\n\nNot planning to add more features, just keeping it simple and easy (and hopefully bug free).\n\nBut if you find bugs or have suggestions, \nfeel free to open an [issue](https://github.com/ryupold/vode/issues) or a pull request.\n\n## License\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryupold%2Fvode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryupold%2Fvode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryupold%2Fvode/lists"}