{"id":25019066,"url":"https://github.com/apleshkov/viewmill","last_synced_at":"2025-07-10T07:02:54.053Z","repository":{"id":188541000,"uuid":"677713304","full_name":"apleshkov/viewmill","owner":"apleshkov","description":"Transform jsx/tsx files to reactive views in js/ts to use in Web Components, insert into DOM or integrate with other libraries/frameworks","archived":false,"fork":false,"pushed_at":"2024-01-04T13:12:51.000Z","size":2109,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-21T19:17:36.466Z","etag":null,"topics":["dom","jsx","mvvm","reactive","rust","swc","tsx","view","wasm","web-components"],"latest_commit_sha":null,"homepage":"","language":"Rust","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/apleshkov.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}},"created_at":"2023-08-12T11:42:09.000Z","updated_at":"2024-08-20T03:18:46.000Z","dependencies_parsed_at":"2025-04-13T03:37:10.683Z","dependency_job_id":"5f29743e-f36f-49df-bf21-5f91936faec3","html_url":"https://github.com/apleshkov/viewmill","commit_stats":null,"previous_names":["apleshkov/viewmill"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/apleshkov/viewmill","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apleshkov%2Fviewmill","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apleshkov%2Fviewmill/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apleshkov%2Fviewmill/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apleshkov%2Fviewmill/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apleshkov","download_url":"https://codeload.github.com/apleshkov/viewmill/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apleshkov%2Fviewmill/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264543442,"owners_count":23625381,"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","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","jsx","mvvm","reactive","rust","swc","tsx","view","wasm","web-components"],"created_at":"2025-02-05T11:19:57.833Z","updated_at":"2025-07-10T07:02:53.981Z","avatar_url":"https://github.com/apleshkov.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# viewmill\n\n**[Features](#features) | [Installation](#installation) | [Getting Started](#getting-started) | [Notes](#notes) | [Examples](#examples)**\n\n`viewmill` is aimed to create complex UIs from a simple form of JSX. It *statically transforms* `*.jsx` and `*.tsx` files to *reactive* views in JavaScript or TypeScript correspondingly, so they could be easily [used](#web-components) in Web Components, inserted into DOM or integrated with other libraries and frameworks.\n\nIn other words, the tool *transpiles* JSX to a code in JS, which creates its DOM nodes from generated [templates](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) and manipulates them *directly*, so there's no Virtual DOM.\n\nYou can think of the views in terms of [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel): after being instantiated, they could be inserted into DOM via the `insertTo` method (the *view*) and their state could be modified by updating parameters of the `model` field (the *viewmodel*).\n\n*Note*: a view cannot update its state from the inside.\n\nThe tool is written in Rust and based on [swc](https://swc.rs) (Speedy Web Compiler) to parse and emit code.\n\n## Features\n\n`viewmill` supports all the valid JSX syntax, including:\n\n- **conditionals** via the ternary operator:\n```tsx\n    {loading\n        ? \u003cspan\u003eLoading...\u003c/span\u003e\n        : \u003cstrong\u003eLoaded!\u003c/strong\u003e}\n```\n\n- **conditionals** via the logical AND (`\u0026\u0026`) operator:\n```tsx\n    {loading \u0026\u0026 \u003cspan\u003eLoading...\u003c/span\u003e}\n```\nUnder the hood `viewmill` considers it a short form of the ternary operator, when the third operand is `null`, e.g. `a \u0026\u0026 b` is actually `a ? b : null`.\n\n- **loops** via the spread child syntax:\n```tsx\n    \u003c\u003e{...items}\u003c/\u003e\n    \u003cul\u003e\n        {...items.map((entry, idx) =\u003e (\n            \u003cli\u003e{idx + 1}: {entry}\u003c/li\u003e\n        ))}\n    \u003c/ul\u003e\n```\n\n- [custom components](#custom-components)\n\nThere're no non-standard HTML attributes or other specific syntax, but it's worth to see the corresponding [notes](#html).\n\n## Installation\n\n```sh\nnpm i --save-dev viewmill \u0026\u0026 npm i viewmill-runtime\n```\n\n## Getting Started\n\nTo demonstrate how the tool works, its basic principles and how to use it, let's create a counter :)\n\nFirst of all we create a directory for our project and call `npm init` there:\n\n```sh\nmkdir counter\ncd counter\nnpm init\n```\n\nThen we install `viewmill` (as a *developer* dependency):\n```sh\nnpm i --save-dev viewmill\n```\n\n... and its **runtime** (as a dependency to use in production):\n```sh\nnpm i viewmill-runtime\n```\n\nLet's create an `src` directory and put our `counter.tsx` there:\n```tsx\n// src/counter.tsx\n\nexport default (count: number) =\u003e {\n    return \u003c\u003e\n        \u003ch1\u003eCounter\u003c/h1\u003e\n        \u003cp\u003eThe current value is \u003cstrong\u003e{count}\u003c/strong\u003e!\u003c/p\u003e\n    \u003c/\u003e;\n}\n```\n\nThen transform it with `viewmill` by calling:\n```sh\nnpx viewmill --suffix \"-view\" src\n```\n\nIt'll create a file `src/counter-view.ts`:\n```ts\n// src/counter-view.ts\n\nimport * as viewmill from \"viewmill-runtime\";\n\nexport default function(count: number) {\n    return viewmill.view({\n        count: viewmill.param(count)\n    }, ({ count }, unmountSignal) =\u003e {\n        return [\n            viewmill.el(\"\u003ch1\u003eCounter\u003c/h1\u003e\"),\n            viewmill.el(\"\u003cp\u003eThe current value is \u003cstrong\u003e\u003c!\u003e\u003c/strong\u003e!\u003c/p\u003e\", (container, unmountSignal1)=\u003e{\n                const p__1 = container.firstChild;\n                const strong__1 = p__1.firstChild.nextSibling;\n                const anchor__1 = strong__1.firstChild;\n                viewmill.unmountOn(unmountSignal1, viewmill.insert(viewmill.expr(()=\u003e(count.getValue()), [\n                    count\n                ]), strong__1, anchor__1));\n            })\n        ];\n    });\n};\n```\n\nOk, so now we need to bundle our code and finally look at it. One could choose not to bundle and arrange everything manually, but here we're going to use [esbuild](https://esbuild.github.io):\n```sh\nnpm i --save-dev esbuild\n```\n\nThen let's create two additional files: `src/index.html` and `src/index.ts`.\n\n```html\n\u003c!-- src/index.html --\u003e\n\n\u003chtml\u003e\n\u003chead\u003e\n    \u003ctitle\u003eCounter\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\u003c/div\u003e\n    \u003cscript src=\"../dist/index.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n```ts\n// src/index.ts\n\nimport Counter from \"./counter-view\";\n\n// you can also use the `new` operator here, e.g. `new Counter(0)`\nconst view = Counter(0);\n\nview.insertTo(document.getElementById(\"app\"));\n```\n\nRun the bundler:\n```sh\nnpx esbuild src/index.ts --bundle --outdir=dist --target=es6\n```\n\nSo finally we can open `index.html` in a browser and it'll show the counter telling us \"The current value is 0!\".\n\nLet's modify `src/index.ts` a bit:\n```ts\n// src/index.ts\n\nimport Counter from \"./counter-view\";\n\nconst view = Counter(0);\nview.model.count.setValue(1);\n\nview.insertTo(document.getElementById(\"app\"));\n```\n\nWe need to re-run the transformation and bundling commands, and then refresh the opened page. Now it says \"The current value is 1!\" and that's correct.\n\nNow let's make things more dynamic and add a button to increment the counter:\n```tsx\n// src/counter.tsx\n\nexport default (count: number) =\u003e {\n    return \u003c\u003e\n        \u003ch1\u003eCounter\u003c/h1\u003e\n        \u003cp\u003eThe current value is \u003cstrong\u003e{count}\u003c/strong\u003e!\u003c/p\u003e\n        \u003cdiv\u003e\n            \u003cbutton\u003eIncrement\u003c/button\u003e\n        \u003c/div\u003e\n    \u003c/\u003e;\n}\n```\n\nBut how to handle the click?\n\nRemember, we cannot mutate parameters from inside the view, so if we write smth like:\n```jsx\n\u003cbutton onclick={() =\u003e (count += 1)}\u003eIncrement\u003c/button\u003e\n```\n\n... it **won't work**, because the handler will be transformed to `() =\u003e (count.getValue() += 1)`, which is invalid and meaningless.\n\nWhat's the correct way? So `viewmill` supports two basic patterns for that:\n1. Provide it as a parameter\n2. Query necessary node or nodes with a selector\n\n### Event Handler via Parameter\n\n```tsx\n// src/counter.tsx\n\nexport default (count: number, onClick: (e: Event) =\u003e void) =\u003e {\n    return \u003c\u003e\n        \u003ch1\u003eCounter\u003c/h1\u003e\n        \u003cp\u003eThe current value is \u003cstrong\u003e{count}\u003c/strong\u003e!\u003c/p\u003e\n        \u003cdiv\u003e\n            \u003cbutton onclick={onClick}\u003eIncrement\u003c/button\u003e\n        \u003c/div\u003e\n    \u003c/\u003e;\n}\n```\n\n```ts\n// src/index.ts\n\nimport Counter from \"./counter-view\";\n\nconst view = Counter(0, () =\u003e {\n    view.model.count.updateValue((c) =\u003e (c + 1));\n});\n\nview.insertTo(document.getElementById(\"app\"));\n```\n\n### Query Selector \u0026 Event Listener\n\n```tsx\n// src/counter.tsx\n\nexport default (count: number) =\u003e {\n    return \u003c\u003e\n        \u003ch1\u003eCounter\u003c/h1\u003e\n        \u003cp\u003eThe current value is \u003cstrong\u003e{count}\u003c/strong\u003e!\u003c/p\u003e\n        \u003cdiv\u003e\n            \u003cbutton\u003eIncrement\u003c/button\u003e\n        \u003c/div\u003e\n    \u003c/\u003e;\n}\n```\n\n```ts\n// src/index.ts\n\nimport Counter from \"./counter-view\";\n\nconst view = Counter(0);\n\nconst { querySelector } = view.insertTo(document.getElementById(\"app\"));\n\nquerySelector(\"button\")?.addEventListener(\"click\", () =\u003e {\n    view.model.count.updateValue((c) =\u003e (c + 1));\n});\n```\n\nThe `querySelector` method uses the very standard [CSS selectors](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors).\nThere's the `querySelectorAll` method also.\n\n### Removing \u0026 Unmounting\n\nIf at some point an inserted view should be removed, the `insertTo` method returns the necessary functionality:\n```ts\nconst {\n    // Every insertion is associated with a specific `AbortController`, \n    // which is aborted on `remove` or `unmount`, so this property is \n    // its `AbortSignal`\n    unmountSignal,\n    // Removes the inserted nodes from DOM and triggers the signal\n    remove,\n    // Unmounts the inserted nodes without the actual removing and triggers the signal.\n    // Useful if there's no need to affect DOM.\n    unmount\n} = view.insertTo(...);\n```\n\nSo if we need to remove the counter:\n```ts\nconst { unmountSignal, remove } = view.insertTo(document.getElementById(\"app\"));\n\nunmountSignal.addEventListener(\"abort\", () =\u003e console.log(\"Bye!\"));\n\nremove();\n```\n\n### Web Components\n\nThe `viewmill` views are intended to be a part of Web Components. So here's an axample of how to create one for the counter:\n```ts\n// src/my-counter.ts\n\nimport { InsertedView } from \"viewmill-runtime\";\nimport Counter from \"./counter-view\";\n\ncustomElements.define(\"my-counter\", class extends HTMLElement {\n\n    private view = Counter(0);\n\n    private insertedView?: InsertedView;\n\n    private get counter() {\n        return this.view.model.count;\n    }\n\n    connectedCallback() {\n        if (this.isConnected) {\n            const inserted = this.view.insertTo(this);\n            const {\n                unmountSignal: signal,\n                querySelector\n            } = inserted;\n            querySelector(\"button\")?.addEventListener(\"click\", () =\u003e {\n                this.counter.updateValue((c) =\u003e c + 1);\n            }, { signal });\n            //   ^^^^^^ Please, note how we use the signal here\n            this.insertedView = inserted;\n        }\n    }\n\n    disconnectedCallback() {\n        // The element and all its children are being removed here,\n        // so it's ok just to unmount the view to trigger the `unmountSignal`\n        this.insertedView?.unmount();\n        this.insertedView = null;\n    }\n\n    static get observedAttributes() {\n        return [\"value\"];\n    }\n\n    attributeChangedCallback(name: string, _?: string, newValue?: string) {\n        if (name === \"value\") {\n            this.counter.setValue(+newValue);\n        }\n    }\n});\n```\n\nThen we can modify the `index.ts` file:\n```ts\n// src/index.ts\n\nexport * from \"./my-counter\";\n```\n\n... and the `index.html` file:\n```html\n\u003c!-- src/index.html --\u003e\n\n\u003chtml\u003e\n\u003chead\u003e\n    \u003ctitle\u003eCounter\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cscript src=\"../dist/index.js\"\u003e\u003c/script\u003e\n    \u003cmy-counter\u003e\u003c/my-counter\u003e\n    \u003cmy-counter value=\"123\"\u003e\u003c/my-counter\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Custom Components\n\nEvery custom component is just a function with the `props` argument, which returns an `Insertable`:\n```ts\nfunction \u003cProps extends object\u003e(props: Props): Insertable;\n```\n\nChildren are available via the `children` property. The value can be an `Insertable`, an array of them or `undefined`.\n\nLet's see how we can extend things with custom components by examples. Please, note how actively the `viewmill-runtime` library is used.\n\n### If\n\nThis component helps us to code condtions with JSX.\n\n```ts\n// src/if.ts\n\nimport { Insertable, Insertion, Live, Unmounter, insert } from \"viewmill-runtime\";\n\n// Show children when the `test` is truthy\nexport default (\n    { test, children }: {\n        test: unknown | Live\u003cunknown\u003e,\n        children?: Insertable | Insertable[]\n    }\n): Insertable =\u003e {\n    // `Live` is a non-static value\n    if (test instanceof Live) {\n        // This wrapper is for working with DOM\n        return new Insertion((target, anchor) =\u003e {\n            // An anchor to avoid jumping around when updating, cause\n            // the content could be re-inserted at the wrong place\n            const a = target.insertBefore(\n                document.createComment(\"if\"),\n                anchor\n            );\n            // This controller helps to stop listening to the\n            // `test` updates on unmount\n            const abortController = new AbortController();\n            // This is needed to remove the inserted `children`\n            let un: Unmounter | null = null;\n            // Introducing a handler here to not repeat ourselves\n            const update = () =\u003e {\n                if (test.getValue() \u0026\u0026 !un) {\n                    un = insert(children, target, a);\n                } else {\n                    un?.(true);\n                    un = null;\n                }\n            };\n            // Listening to the `test` updates\n            test.listen(update, abortController.signal);\n            // Check the `test` value on a first insertion\n            update();\n            // The `Insertion` callback needs to return an unmounter\n            // to clean up its things if necessary\n            return (removing) =\u003e {\n                abortController.abort();\n                un?.(removing);\n                if (removing) {\n                    target.removeChild(a);\n                }\n            };\n        });\n    } else if (test) {\n        // The `test` value is static, so let's just\n        // return `children` if it's truthy\n        return children;\n    }\n    // `Insertable` could be undefined, so no need to return anything here\n};\n```\n\nHere's an example how to use it:\n```tsx\nimport If from \"./if\";\n\nexport default (count: number) =\u003e {\n    return \u003c\u003e\n        \u003cIf test={count \u003e 10}\u003e\n            The \u003ccode\u003ecount\u003c/code\u003e is {count}, which is\n            obviously greater then 10!\n        \u003c/If\u003e\n        \u003cIf test={123}\u003e\n            \u003cp\u003eThat's truthy!\u003c/p\u003e\n        \u003c/If\u003e\n    \u003c/\u003e;\n}\n```\n\n### For\n\nIterating over an array using a function, which is provided as a child.\n\n```ts\n// src/for.ts\n\nimport { Insertable, Insertion, Live, Unmounter, insert } from \"viewmill-runtime\";\n\nexport default \u003cE extends Insertable\u003e(\n    { items, using }: {\n        items: E[] | Live\u003cE[]\u003e,\n        using: (item: E, index: number) =\u003e Insertable\n    }\n): Insertable =\u003e {\n    if (items instanceof Live) {\n        return new Insertion((target, anchor) =\u003e {\n            // The content is being removed on every update here too,\n            // so we need this anchor to stabilize the placement\n            const a = target.insertBefore(\n                document.createComment(\"for\"),\n                anchor\n            );\n            let unmounters: (Unmounter | null)[] = [];\n            const unmount = (removing: boolean) =\u003e {\n                unmounters.forEach((u) =\u003e u?.(removing));\n                unmounters = [];\n            };\n            const update = () =\u003e {\n                // Remove the previously inserted items if any\n                unmount(true);\n                // Insert the new ones\n                unmounters = items.getValue()\n                    .map(using)\n                    .map((entry) =\u003e insert(entry, target, a));\n            };\n            const abortController = new AbortController();\n            items.listen(update, abortController.signal);\n            update();\n            return (removing) =\u003e {\n                abortController.abort();\n                unmount(removing);\n                if (removing) {\n                    target.removeChild(a);\n                }\n            };\n        });\n    } else {\n        // Just a static iterable\n        return items.map(using);\n    }\n};\n```\n\nBoth static and non-static usages:\n```tsx\nimport For from \"./for\";\n\nexport default (items: string[]) =\u003e {\n    return \u003c\u003e\n        \u003cul\u003e\n            \u003cFor\n                items={items}\n                using={(s, idx) =\u003e \u003cli\u003e#{idx}: {s}\u003c/li\u003e}\n            /\u003e\n        \u003c/ul\u003e\n        \u003cFor\n            items={[1, 2, 3]}\n            using={(n) =\u003e \u003c\u003e\u003cbr /\u003enumber: {n}\u003c/\u003e}\n        /\u003e\n    \u003c/\u003e;\n}\n```\n\n### Extendable List \u0026 `userData`\n\nIt's possible to enrich a component behaviour, using `userData` while listening to a live param.\n\nFor instance let's create an extendable list, so it's possible to add items there without its full re-rendering:\n```ts\n// src/xlist.ts\n\nimport { Insertable, Insertion, Live, Unmounter, insert } from \"viewmill-runtime\";\n\nexport default function \u003cE extends Insertable\u003e(\n    { items, using }: {\n        items: E[] | Live\u003cE[]\u003e,\n        using: (item: E, index: number) =\u003e Insertable\n    }\n): Insertable {\n    if (items instanceof Live) {\n        return new Insertion((target, anchor) =\u003e {\n            // Let's use a container here to show how to unmount\n            // its children\n            const container = document.createElement(\"div\");\n            let unmounters: (Unmounter | null)[] = [];\n            const unmount = (removing: boolean) =\u003e {\n                unmounters.forEach((u) =\u003e u?.(removing));\n                unmounters = [];\n            };\n            const insertItems = (items: E[]): (Unmounter | null)[] =\u003e (\n                items\n                    .map(using)\n                    .map((entry) =\u003e insert(entry, container))\n            );\n            const update = (tail?: unknown) =\u003e {\n                if (typeof tail === \"number\" \u0026\u0026 tail \u003e 0) {\n                    // Just insert the new items\n                    unmounters.push(\n                        ...insertItems(items.getValue().slice(-tail))\n                    );\n                } else {\n                    // The defalt behaviour is to replace everything\n                    unmount(true);\n                    unmounters = insertItems(items.getValue());\n                }\n            };\n            const abortController = new AbortController();\n            // Handling `userData` on every change\n            items.listen(({ userData }) =\u003e update(userData), abortController.signal);\n            update();\n            // Don't forget to insert the container\n            target.insertBefore(container, anchor);\n            return (removing) =\u003e {\n                abortController.abort();\n                // No need to remove the inserted items no matter what\n                // the `removing` argument is, cause they're all children\n                // of the container, ...\n                unmount(false);\n                if (removing) {\n                    // ... which is being removed here\n                    target.removeChild(container);\n                }\n            };\n        });\n    } else {\n        return items.map(using);\n    }\n};\n```\n\nNumeric list view:\n```tsx\n// src/xnumlist.tsx\n\nimport ExList from \"./xlist\";\n\nexport default (items: number[], onClick: () =\u003e void) =\u003e (\n    \u003c\u003e\n        \u003cExList\n            items={items}\n            using={(n) =\u003e \u003c\u003e\u003cbr /\u003e{n}\u003c/\u003e}\n        /\u003e\n        \u003cp\u003e\n            \u003cbutton onclick={onClick}\u003eLoad next\u003c/button\u003e\n        \u003c/p\u003e\n    \u003c/\u003e\n);\n```\n\nThe button adds new 3 items to the list on every click:\n```ts\n// src/index.ts\n\nimport ExNumList from \"./xnumlist-view\";\n\nconst xlist = ExNumList([1, 2, 3, 4], () =\u003e {\n    const { items } = xlist.model;\n    const n = 3;\n    items.updateValue(\n        (current) =\u003e {\n            const lastItem = current[current.length - 1];\n            // Generate next items\n            const next = Array.from({ length: n }, (_, k) =\u003e lastItem + k + 1);\n            return current.concat(next);\n        },\n        // Here goes the `userData` value, so the view'll insert\n        // only the last `n` items of the updated value\n        n\n    )\n});\n\nxlist.insertTo(document.getElementById(\"app\"));\n```\n\n### Fetching Data\n\nA very simple component to fetch remote data:\n```ts\n// src/fetcher.ts\n\nimport { Insertable, Insertion } from \"viewmill-runtime\";\n\nexport default function ({ url }: { url: string }): Insertable {\n    return new Insertion((target, anchor) =\u003e {\n        const abortController = new AbortController();\n        const info = document.createElement(\"div\");\n        info.textContent = \"Loading...\";\n        fetch(url, { signal: abortController.signal })\n            //       ^^^^^^ Stop fetching if aborted\n            .then(() =\u003e info.textContent = \"Loaded!\")\n            .catch((e) =\u003e info.textContent = `[ERROR] ${e}`);\n        target.insertBefore(info, anchor);\n        return (removing) =\u003e {\n            // Aborting the controller on unmount\n            abortController.abort();\n            if (removing) {\n                target.removeChild(info);\n            }\n        };\n    });\n}\n```\n\nFetching one remote url and a local one:\n```tsx\n// src/fetching.tsx\n\nimport Fetcher from \"./fetcher\";\n\nexport default () =\u003e (\n    \u003c\u003e\n        \u003cFetcher url=\"https://github.com\" /\u003e\n        \u003cFetcher url=\"/\" /\u003e\n    \u003c/\u003e\n);\n```\n\n```ts\n// src/index.ts\n\nimport Fetching from \"./fetching-view\";\n\nconst view = Fetching();\n\nview.insertTo(document.getElementById(\"app\"));\n```\n\n## Notes\n\n### Typescript Configuration\n\n```json\n{\n    \"compilerOptions\": {\n        \"jsx\": \"preserve\",\n        \"jsxImportSource\": \"viewmill-runtime\"\n    }\n}\n```\n\nThe `jsxImportSource` option here fixes the `JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. ts(7026)` error if [`noImplicitAny`](https://www.typescriptlang.org/tsconfig#noImplicitAny) or [`strict`](https://www.typescriptlang.org/tsconfig#strict) enabled.\n\n### HTML\n\n#### [Boolean Attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)\n\nLet's consider a simple example:\n```tsx\nexport default (flag: boolean) =\u003e (\n    \u003cinput type=\"checkbox\" checked={flag} /\u003e\n);\n```\n\nAs a result, the `checked` attribute is present if the `flag` value is `true`, and is absent otherwise.\n\nIt also works for the spread attributes syntax:\n```tsx\nexport default (flag: boolean) =\u003e (\n    \u003cinput type=\"checkbox\" {...{ checked: flag }} /\u003e\n);\n```\n\nPlease, note if a value is `null` or `undefined` it's necessary to convert it explicitly:\n```tsx\nexport default (flag?: boolean | null) =\u003e (\n    \u003cinput type=\"checkbox\" checked={!!flag} /\u003e\n);\n```\n\n#### Remove Attribute\n\nJust set its value to `false` as it's shown in the section [above](#boolean-attribute).\n\n#### Toggle Attribute\n\nThere's no specific syntax for that, but you can introduce a custom function:\n```ts\nexport function cls(v: Record\u003cstring, boolean | undefined | null\u003e): string | false {\n    const c = Object.keys(v).filter((k) =\u003e !!v[k]);\n    return c.length \u003e 0 ? c.join(\" \") : false;\n}\n```\n\nSo it can be used like:\n```tsx\nimport { cls } from \"./utils\";\n\nexport default (a?: boolean | null) =\u003e (\n    \u003cdiv class={cls({ enabled: a, disabled: !a })}\u003e\n        \u003ccode\u003e{a}\u003c/code\u003e\n    \u003c/div\u003e\n);\n```\n\n## Examples\n\n### [Table](https://github.com/apleshkov/viewmill/tree/main/examples/table/)\n\nA table with sorting and paging. \n\n- The whole view is defined in just one file (see `src/table.tsx`)\n- Written in Typescript\n- Data from [Random User Generator](https://randomuser.me)\n- CSS by [Bootstrap](https://getbootstrap.com)\n- Bundled and served with [esbuild](https://esbuild.github.io)\n\n![Demo](https://github.com/apleshkov/viewmill/blob/main/examples/table/demo.gif)\n\n### [Form](https://github.com/apleshkov/viewmill/tree/main/examples/form/)\n\nA form with dynamic fields and validation. \n\n- The whole view is defined in just one file (see `src/form.tsx`)\n- Written in Typescript\n- CSS by [Bootstrap](https://getbootstrap.com)\n- Bundled and served with [esbuild](https://esbuild.github.io)\n\n![Demo](https://github.com/apleshkov/viewmill/blob/main/examples/form/demo.gif)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapleshkov%2Fviewmill","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapleshkov%2Fviewmill","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapleshkov%2Fviewmill/lists"}