{"id":28076994,"url":"https://github.com/tatomyr/purity","last_synced_at":"2026-02-27T05:33:41.418Z","repository":{"id":37453167,"uuid":"193216367","full_name":"tatomyr/purity","owner":"tatomyr","description":"Pure JS UI framework","archived":false,"fork":false,"pushed_at":"2025-11-14T12:23:01.000Z","size":945,"stargazers_count":18,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-14T14:20:42.998Z","etag":null,"topics":["esm","framework","js","ui"],"latest_commit_sha":null,"homepage":"https://tatomyr.github.io/purity/playground/","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/tatomyr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","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":"2019-06-22T09:33:29.000Z","updated_at":"2025-11-14T12:22:31.000Z","dependencies_parsed_at":"2025-05-13T01:56:34.566Z","dependency_job_id":"f9810ddc-cd84-443b-8bd1-7569b4dd7df2","html_url":"https://github.com/tatomyr/purity","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tatomyr/purity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatomyr%2Fpurity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatomyr%2Fpurity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatomyr%2Fpurity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatomyr%2Fpurity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tatomyr","download_url":"https://codeload.github.com/tatomyr/purity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatomyr%2Fpurity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29885833,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-26T23:51:21.483Z","status":"online","status_checked_at":"2026-02-27T02:00:06.759Z","response_time":57,"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":["esm","framework","js","ui"],"created_at":"2025-05-13T01:56:29.444Z","updated_at":"2026-02-27T05:33:41.410Z","avatar_url":"https://github.com/tatomyr.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Purity. Declarative State \u0026 DOM Manager\n\n![check](https://github.com/tatomyr/purity/actions/workflows/check.yaml/badge.svg)\n![deploy](https://github.com/tatomyr/purity/actions/workflows/deploy.yaml/badge.svg)\n\nDeclarative UI library for using most of today's Javascript.\nIt doesn't require any bundlers or using **npm** at all, and it fully leverages the native ECMAScript modules system.\n\nCheck out our [Playground](https://tatomyr.github.io/purity/playground/) to see **Purity** in action.\n\n## Usage\n\n### Basic Syntax\n\nTo use **Purity** in a project, you have to put in your **index.html** a root element where your app will be mounted into, and a script tag of `[type=module]` which points to the main js file:\n\n```html\n\u003chtml\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\" src=\"./main.js\"\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Purity** exposes two main methods to manipulate an application:\n\n- `init` which initializes the app with a default state (application-wide)\n\n- `render` tag that wraps string templates that represent app components\n\nImport them from the local file or a public URL, e.g.:\n\n```js\nimport {init, render} from \"https://tatomyr.github.io/purity/purity.js\"\n```\n\nNext, you init the app with some default state. This will return a bunch of methods you can use in your app:\n\n```js\nconst {mount, getState, setState} = init(defaultState)\n```\n\nThen you declare a component using the `render` tag:\n\n```js\nconst root = () =\u003e render`\n  \u003cdiv id=\"root\"\u003eHello Purity!\u003c/div\u003e\n`\n```\n\nMake sure that your root element has the same `id` attribute as the root defined in **index.html**.\nThe first will override the latest.\n\nFinally, you have to mount the `root` to DOM:\n\n```js\nmount(root)\n```\n\nThat's it! The simplest possible Purity application is ready to be deployed!\n\n### Nested Components\n\nAs your DOM tree grows, you may want to extract some components.\nSince they are merely bare functions that return a string, we can embed other functions called with some arguments, that return a string:\n\n```js\nconst child = ({name}) =\u003e render`\n  \u003cdiv\u003eHello ${name}!\u003c/div\u003e\n`\n\nconst parent = () =\u003e render`\n  \u003ch1\u003eWelcome page\u003c/h1\u003e\n  ${child({name: \"Guest\"})}\n`\n```\n\nYes, you may return several nodes from a component.\nThey don't necessarily have to be wrapped into one (except for the root one).\n\n### Event Binding\n\nWe can add some interactivity by binding events:\n\n```js\nconst clickable = () =\u003e render`\n  \u003cbutton ::click=${() =\u003e alert(\"Hello!\")}\u003e\n    Click Me\n  \u003c/button\u003e\n`\n```\n\nPlease notice the double-colon syntax. The pattern is `::event-name=${\u003cevent-handler\u003e}`.\n\n**Purity** binds events to DOM asynchronously, so be careful when writing tests.\nYou have to use `await delay(0)` before you can simulate an event after DOM gets updated.\n\nThere is also another substantial limitation to using event handlers.\nDo consider each handler an isolated function that can receive nothing from the upper scopes.\nFor instance, the example below is wrong since we are trying to use `COUNT` (which has been calculated in the component's scope) inside the click handler:\n\n```js\nconst wrongCounter = () =\u003e {\n  const COUNT = getState().count\n\n  return render`\n    \u003cdiv id=\"root\"\u003e\n      \u003cpre id=\"count\"\u003eCounter: ${COUNT}\u003c/pre\u003e\n      \u003cbutton\n        ::click=${() =\u003e\n          setState(() =\u003e ({count: COUNT /* Incorrect value! */ + 1}))}\n      \u003e\n        Increment\n      \u003c/button\u003e\n    \u003c/div\u003e\n  `\n}\n```\n\nAlthough the increment on click _will_ work once, it is not guaranteed to do so every time.\nThe event binds on the first execution, but the _button_ doesn't get updated further, so both the event handler and its _closure_ remain the same.\n\nThe correct example would look like this:\n\n```js\nconst correctCounter = () =\u003e {\n  const COUNT = getState().count\n\n  return render`\n    \u003cdiv id=\"root\"\u003e\n      \u003cpre id=\"counter\"\u003eCounter: ${COUNT}\u003c/pre\u003e\n      \u003cbutton\n        ::click=${() =\u003e\n          setState(({count}) =\u003e ({count: count /* Correct value! */ + 1}))}\n      \u003e\n        Increment\n      \u003c/button\u003e\n    \u003c/div\u003e\n  `\n}\n```\n\nPlease notice that `setState`'s callback receives the current state as an argument.\n\nOne more important thing to notice is that the `pre` tag has an `id` attribute defined.\nThis allows to only update its content without re-rendering other nodes that don't have visual changes.\nThis helps the `button` not to lose focus on each click.\nSee more in the [Virtual DOM](#virtual-dom) section.\n\n### Async Flow\n\nYou can implement the simplest async flow using a tiny helper (you may also import it from [once.js](src/once.ts)):\n\n```js\nconst makeOnce = () =\u003e {\n  let lastCalledKey\n  return (key, query) =\u003e {\n    if (lastCalledKey !== key) {\n      lastCalledKey = key\n      setTimeout(query)\n    }\n  }\n}\n```\n\nwhere `key` is a unique identifier of the async operation and `query` is an asynchronous callback function which gets executed once the `key` changes.\nIt can be used like this:\n\n```js\nconst {mount, getState, setState} = init({\n  spinner: false,\n  stargazers_count: \"-\",\n  url: `https://api.github.com/repos/tatomyr/purity`, // could be changed somewhere in the app\n})\n\nconst getStargazers = async () =\u003e {\n  try {\n    setState(() =\u003e ({spinner: true}))\n    const {stargazers_count} = await fetch(url).then(checkResponse)\n    setState(() =\u003e ({stargazers_count, spinner: false}))\n  } catch (err) {\n    setState(() =\u003e ({stargazers_count: \"🚫\", spinner: false}))\n  }\n}\n\nconst once = makeOnce()\n\nconst root = () =\u003e {\n  const {url, spinner, stargazers_count} = getState()\n  once(url, getStargazers)\n\n  return render`\n    \u003cdiv id=\"root\"\u003e\n      \u003cpre id=\"stars\"\u003e\n        ${spinner ? \"⌛\" : `⭐️: ${stargazers_count}`}\n      \u003c/pre\u003e\n      \u003cbutton ::click=${getStargazers}\u003e\n        Refetch\n      \u003c/button\u003e\n    \u003c/div\u003e\n  `\n}\n\nmount(root)\n```\n\nYou may also check out [the imperative example](https://tatomyr.github.io/purity/playground/#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-U8BHJjVVPyIQ6pRSNS6pl7U4CdQxj0XEXR-HnzKbDCp2LQjVN6BzBAxzKo2XMDVbUI65jVRCPFOAmr1nwAYFs6IooLlXOQDcwJdznAPOwIqu2CMqkZr8FmytM6YCtpuHbDmViahaIhC4d02AWw7PPdW+A6ghl2ScpyRVHMkW6PkIYgB2BgGQv7RB9YyAICAOTba2x7kkOaDgAANncih9DKHDH-rPoGk1tD51avgDqsREigwGtkcaytZrQgWuA0YuAVj8GHrTf2k9aN8OVqOb1HNPBhpcEpjcAI3wlqIlWgVTm76ap1vJZWuGnrjinHOGAjlvQj3ptRlm3jA1+ORBGl6sB41DNQFTq7ZwS9qip3ThcK9cAXheTgBYOgNRSAUGoHQfhQiMhTvkBFQjHH2bdpzBWxt06P2yeo2F+QPHuaJBM9AOdid+2Y2c0jSTkb+6VpHicFw+IhZKoDTFQgszegywXmEOAR8a7bTvkrVWKAmAqwLggQts9j6b0ANV7cBTaFpmbnbNM856AGxidex9AAK1IAeD+evDuk7Os0xc+I4Gq3AEuFWVvlfEYrZWat60wA1pW5rdtZa1Za1x6LVLWW0r8Bhklx8qMhAQcGpR0RTbnee9gllbLuAoDgIY9l5pFuneO6t-okQzt7YI67O0OzfbxH9jLHcPgZancnvlGwUBIYQAJMgIHy2S5I8rotsHG2xDrZ6CfRzfAIDeFqKjeHJWeibcq-VtW5EYBHZrlYeoV0VvtFq768J5hFhFrgCvYGABuEH1cysdJ59gFbUySaMytieaAKMZftEnpW-zGumdk-EbbKWShfI1ViOz1IQA)\n(or alternatively [the declarative one](https://tatomyr.github.io/purity/playground/#JYWwDg9gTgLgBAb2AO2DANHKBTZATbKAXzgDMoIQ4ByAOloHoUCAPWgKwGdqAoHgYwjJO8BCAgBXZBiyFcBYnAC8cFGgAUASh5weDBnABUxk6bPmLlq9Zvm+g4fBABDANbYAgpwCeyfsrh1HBx8Qk1lAD5EHTgHEVjnABtEzgCEIhicGAkoZEDgPEwARwlCb3ClKIQYuFiheP4kxID1PGcYZwqqmtqE5M4AbQKAXTT6Wkb+obxhzBF2iU4ALhoweRQAc2pMQgooFakCUhRsPAze3pKyrR6L2hgAC1x1VvbOyL6U6dGVBHns5Y0TgSfj8bCcbiYNodIjaC53RowfgPF67aBdT6DEZpf6LFbUUjOYCJHLYbZwNHEOHw2q0Y7IJreIJyUJQam1c69YCkQIAQkmXxG4WqNIFNwuGR6WRyeRF8PGAqxM0wt1kpGwSIeK1eHQxctFTR1nQA3LoabVgvJCFpTarORyYhlJfojLY3e6PZ7DDoBPV4ItPD4-AEXO4vL5+MyQgptL7HLEnvxXAAlcGQYTYAI4VKVaK1bl87O0CCuYU9RIailQKABZDYADucAAotXoOoAEQACQAKt2AApwXGpQQEFbtuAAalknFoQ-ZVagtGz6c4mZU2Z6u1nHQBWfB24WnB6jwojd2jsyGpl044nCEN0lcX9UGaKgABg8YDAwMt9M4wMAtAbGgDwSAARhMlAMDgkCcAwO6UN4UAMGAORoN4b72H6qhxGkZqDiCYIQvigAK1IA8H-bDEhLEqS+KALwbgDVe5RtRrPgmz4oA2MSUY+2EUBA8AqFoHxyk+iBDlCbw7K2UCYDg6qaiQKgBuGfjqDkiSYEJubyciakvpo9xPMg6jItgSappwK7YJomj6LQ2AsGZEgwNg4p1PGeDcjyKjQs4AD8B5QBszgAF6EJwAD6ghSPAAC0gSJBAkwAMowNAzgbNggXBWFUCRdF0hwH5flwAADNSBatF54SJSlaVQBlWXzEFoXhVFkiFT5bwBc1OVtQVMB8BaV65LIrKYRcAA8nkAG6qHgSjtnxMDthEqqTWAODzYtzWcKtqq1AAJEgcQDEOwwkMdvk9R0LW5flHXwMVNB+dQRBXV5RVwG+cCTWBETqBOH2kKQsKTQw-0-fib3rShOBrTS03AHNBSLZSq2TcAUQHXAx1DsoSgqASRIkjg1BwAAZBTgSUgFIDgpwjVFSVlKaPavTg1jSMzQj8J-c5aV5DjSxLPwiTAEmSjHXJGrIuztS8zSqY6Q8sNgQLQiK79DCzbzmGSuIMVBBA-HaEAA))\nand [the complex useAsync example](./src/examples/async-counter/index.js) for advanced cases.\n\n\u003c!-- TODO:\nhttps://tatomyr.github.io/purity/playground/#JYWwDg9gTgLgBAb2AO2DANHKBTZATbKTAM2hAEMYAVUbAXzmKghDgHIA6DgehQIA8OAKwDObAFDju3OACp5CxUuUrVa9RuWSAxhGQj4FANbYA8sm3Y4AXjgAKAJQ2AfInFw4u-fG3kANn4iNnDI2ADucADK2DCO7lgxAK5QyPbAeJgAjomEAJ5O1q4I8R7AxPYAhL4BIhwAFuQidukOTsUeHZ7+gRzkeHjNeA4A3CUdIjE0INgQibHZeQ5jdPErK+JeBnATMABK2EzYInXBzcgwBa47AJLnhABu-nY4OPiEmCgXOnpbAOYxADFmCAADIQaqRGDQcj-U4mfIuNwePwxOB4SjkODxGBQXJIjromCY2wAKUipgAchwwOQoBM7H5wf5IdD-hx-jBrjBsCA7PDWqsujBtCc7IQoG0Vh4cDBkqlCZiAD6KxBrb7ebaTCBgiFQqAwqy2PnYXKYBWXfGM3Ws7AcG7c3nwzBkyl2nEoX5lXJ2c1LdbSOSaIPBkOh2TqrbJPzBAAGdRgMDAIgAXNJyGBgOy0HVEgAjDi6EDcHCQETcIlQkC5KDcMDJNC5GMR+AIECzc6YDmQyjYTA7bvczAvXAEKAMWwoNB2dpwLjswHAnXMvUGuxRhzobG0W7cqCPPzJuAABk3HhEGeQoSgh+I-gmp7g4ugh8Sb2IKGwGXEdCWGx+8D0SxgmMMwLGwOI-w1EVsG0Ix9nPH5DQSIJCnxMpKhwWoICMNoxhReBxWCUIIgAUSgZgoDsAAiAAJKgqAABW2CtEiCXQCEPKi4AAamQt1KFYpZOnFDhMMgfQkMwsYRIMASUL42TZREMYYDqZgInFVZ4hlOU+NEPQ4nWTZ4C7IkoF+cgAC9CHkxpcgsewLRnfsK3AxxEWnc8UCvQ8cRyTAn2vOBXwId9QgyNEymIQ8jx-IS4BxPEZw8YzEFk8yrJsgB9XRXxgcc4HIMJyDQRgYhFNcoD8BwOFU3A7Gg2D4PEiZ4pS-9IuIcpbHSizrLpHL23gABaOBTJ7Rx+Iy-qREGvKxlS7RkleGAAGE6hgowABEe2CHbuQ4ZAIDCOJOlSmBtzuPd-GCcbuUmvxGjWjbYP2qwADJ3vsJbyNwZ7NreuBRru8Casegx1oBns2s1GABzci1PLMvrstyjs4HB-7Xp7Q8fpWyHscHbYLx8xg717TriEwC7ph3B5-DisYdiobUmT8Fl9X+Sq-EwBBesyga0YwDGnoJ7acc8Za-rFt6f0FXxhTqMVyNwzoXIm9zUM8knCBvcmAvI59H3IgB+DhphEEQDTgE2TeNscBQ8NVIK2ZgIHgI0nPiQDwKjTsYm7aabPin27DYHBDmONhME1oomZifZI6VgBmI80-iuX2o1PnkYF2ahb7HWiHt6AzSi6nLt3fdMExmWewKkHTs8DqaewEjHqTT89p7Q7jvc0ba5e8XuUkaUkhSBI3igJtOgAHjwYB7jgdJrCot2YCo5wxg8WewBwZe8FX9KRE37eOgAEgQQKbfYQBeDcAar22DgQ8vMvQgb7YQBsYifw82EABWpADwf2wBgl9+YzTmucOgl8KgVFbnTa60ZPr2AXl1G+MY4Cz1zM4Ow3FL4oOIAwFAcBL6kCgBQagtA7BwKuvuBwP5Z7cCweg3+9hW7Lxof4FhvFSHkKmOBahVd-BOF4lRBwoi4BSk6Bg2sOAt5SIwSICgARnCz2AM4S+gU6AMLUQwxR3QVG5igHAbgcipGYLmFCVIyZkzaD8MAWC1hL7jSDnSOgpj5GJ3KnUM+DDcwWL0O4jou994ryoq3Ya2AO4TDwKfeRHhL6t3bumaJcAKjWFsCFA4H48BwCQTGEET1PBD0-IeEhZBKB8KobQJJnchh0BjJIueMjsCBIYQve4pimzrDbHlZ4EB3ZLCAA\nhttps://tatomyr.github.io/purity/playground/#JYWwDg9gTgLgBAb2AO2DANHKBTZATbKAXzgDMoIQ4ByAOloHoUCAPWgKwGdqAoHgYwjJO8BCAgBXZBjgBzbDADKMAIYxsmTguVqNWQrgLE4AXjgo0ACgQ84duCJVRZKgF6FOAfUFSYALhoAWmpMW3sfaQCABnQwuxxITgCAbQBdWPsHbAAbbH51PAAlbEgAqQJSFGw8DPtOMBRkQgDSFWytWKIASj4GBjgAKiHhkdGx8YnJqemxvkFheBAVAGtsAHlkfmxTOEsu0wA+RDi4eZFTtvadhCI+TJwYCShkXeAauABHCUIAT32TI42TKZYCkXYAQn4l04yTeqX2QOBSKh2XasLwqR2MCg3xOSLsfU+3ygPz2cDx+K0MAAKqBsJIYJYvr8evi4LdMrdbgIhOchFsdktVhstntev0ZpKpdKZUM5rz4GswDBgEIdtZkCoQHo8MBOCoAEa5PDdQ76fCEAAGJwAPBBlaqXgA3NrfEwAIgAJAhNdqiO64N7fdsTKG5NpVOo9rQtLl8tVipA4AAyZM0WN5ArUEje3X6o3VFNp6h5w3G7MHCl2INa7Ac+w2hj2lVCSvWnkLU4ACzyy2K9V5If0nDNiLsoIhOE4tAgywRVbguXghCgOyaAHc4ABRKAUKCWd0ACWp1IACg5IxIR4ICAEAwBqYcxy+cVn4le0KeQYRDqcLj+OI8I5mFOz5qFeC4wF2FCbiuJz1vEChPC8oFcEIYrcmc8DyEoqhQMB+iQAAcrWZqkAo-BdpYlpdjAMBgEkfQqA0tCyGgXYSAatCCCADAJBAnAMJGlA-FADDevxJF+pab52LQUG4JYlG9v235aLJcDyT2yCWNYjjOG4HjeJI0imgCWS4boun-Ecel4S47j4cZvjdF0GncWolHWWaVI6FGZLmXZTgOUZET+EE2Zue5lSaqipIBUcvmRtg3mBQg9SNM0ZBtFork9B25z8ZwACqUDZDsNF0QxfhMSxbFQZx3GUAwV4eEJagiWJRUAPycNAMAmBIYB4LoeDJmAhCeGAKjyCYACMURRO2WFwIoOSZuqNnHJk-IpUVpXZJgCVkBRVH7WVGmZFpinKfwfbYAOP6XfYHkwF5lgrltbSEIyH7apw+ryG5C7XTpRWjgumQrRm8ZFCUEA7D1tDJFEqTdbQwaQ3UEZWcd1hFZoDTIE0UABNi3yaOtsOJhArlY4hjzPFkcYFDTC7dCDCk6ThOj4Rp+X3EhTM4BaUDWmyNow-A9NwH4fiUSoyCzd61j2QoAQIC62TfAEUsJvDRBmYCMuZEluNbXpVOs-DhOZaTcDk3WwNsmyPN4Zwlh63DkDPUihsLkcMvem7VldJ+8OcOjSxgJYSotsgXQIcCjZS5WmTWphCpwAAgmAYCbRDJwPMh5pGOL+I2rqTrmHgHoUBAMDumnLvVgga0s3s3It3YNpgDgNcevpnBNyb9jBzjUZhxlxOEHA3U0IA2MTULLNCAArUgDwf9mgYICHk9gQZjleGFJC85wMuNn32DNy35-QXADCVufBoSHRaqj3Ycv8NkwB3SYKtbWbfyCBj4W2ASZcKYU4CPjmq5JObJr7dzsAAYW-ndc+DBn6v2QAgiWl8DiAD4NwAd7vb13ilMOx84AoJ-ssTgF8cCPwlgwKuCCM58HEL4SwucwA9CAA\nhttps://tatomyr.github.io/purity/playground/#JYWwDg9gTgLgBAb2AO2DANHKBTZATbKAXzgDMoIQ4ByAOloHoUCAPWgKwGdqAoHgYwjJO8BCAgBXZBjgBzbDADKMAIYxsmTguVqNWQrgLE4AXjgo0ACgQ84duCJVRZKgF6FOAfUFSYALhoAWmpMW3tOMBRkQgDSFQAbLXQeIgBKPgYGOAAqXLz8gsKi4pLSssK+QWF4EBUAa2wAeWR+bFM4S1TTAD5EMLsqkTh+BMT2hCI+e30YCShkDuA8TABHCUIATy6TXoQ4fvtgUg6AQhH4xIBtJYBdLptpx+HRzmu8G-aYKHWDp60YAAqoGwkhgljWm3ST0m00mkwEQiGQla7VqDWarU6GSy5VxePxBNyfAR1WGAAtsPw6gAlbARRFtMw4Tg9PrTI6nZm0CB1e6-ezxBRwQhQdrRADucAAolAKFBLAAiAASAIBAAUHKpZizBAQAgq4ABqfScWiObVQp7C2W0ZmQYSMk38uwis1aiQspl0t1qD3OuAwMkUSUig4w+yDCCC2jxCCySwAViTyYTXQOOFm8xNHE4Qix8NIUn4MGAQjgYHiKg2ikk+Esc3ifIGiPgKgkeFLYuwkoAgu3S-WoI2ANxhNsdiC0CtVzqjkiVFtwBvtAAGZJgMDAnD8mRUkVosjQZIkACNaIIQAwcJBOAxaXgIPx4hsr9gH0+NoEn8AVzxMsNF2XMw1w3LcdwYPdgAPI9T3PShXxvBgtUoDYoAYMBxV-Ekhh7MAwHaTpWRsSNo1jeMAEZKKo9IwmRbBB3iTAVE4DYWg6bZdgOEjsBjONLAAZkEoTBLTA5-3+IEQBBCQwUInY4H-AFGgAEUaAJxTJDYAIkeI8GQah4DQOATzaFiWgAfn5f4dHUSw5N6awIiiGIA2+bA0ktOx-y6ewpgjREox4sjLAAFjC8KQs81ytIeR5BlERxnDcDxvFrGASDMFRxRUIzSAUfgyQY1JaEDXBLAKykaTpe0tCi5tSQ7UhjjMRKXHcKAvB8aQ4ECDpDVjc5lGgFR5FtN9H2fTxWuSjrUt8OAAB8FrgAAGOrzGOSxGuOXoVsW5aBoSIaoBGnicHfSbpvazq0tMEwzCkAhSBQN8mytadq1rPBLGoddN23TIH3FZBYxUPAzRUcBBXiYAzwvBhwH4hhOEhitsECfjTUR6h1vDaZDviY7TrGi6Nim1Qkuuubupaim2pSrqYCs7QtXo+yOgQK6GbSzBto8-Yw2eGACssEU3r+FndDsjiOa52bGYCagWGoDzBee5BRhi-luN4+MADYDcNvXUlHJ5xIUSTpNkmX-Ws1npdZRzImQaIoFiBItH5-lvP2J4deCgB2IPg4DqLwzSPz-b4gAmWO49jy0MzmBYcHwQhf2mAAeDsADdzDwEwFQoCAYAVbp+UzsAcHzwvEs4Mv-QAEj2eQlHt4qnJdwg4HMmhAGxiag4EVwAFakAeD+VbgZvW5s+iO7pmabt8EgdA6jop8l2ziu2uBdrgAAyPeaENFX17bqWt6OUgI6eTP0Jwcus4YXOH7gX94XEXxLFwsB0iAA\n--\u003e\n\n### Virtual DOM\n\nBear in mind that each changeable node should have a unique `id` attribute defined on it.\nThis allows the DOM re-renderer to decouple changed nodes and update only them.\nIt has nothing to do with **components**, which are just functions to calculate the HTML.\n\nYou can think of your application as a tree where each tag with the `id` attribute is represented by a **virtual node**.\nThe most important part of the virtual DOM is the **rerenderer**.\nIt calculates new virtual DOM and traverses through each existing **virtual node**.\nIf a new corresponding **virtual node** exists, and it shallowly differs from the previous one, the **rerenderer** replaces `innerHTML` of the **node** and attributes of a wrapper tag.\n\nThis way, the **rerenderer** could preserve text inputs cursor position, scrolling progress, \u0026c.\nAt the same time, it allows a programmer to fully control the updating process.\n\nDOM nodes get re-rendered depending on how `id`s are placed across them.\nBasically, **Purity** will re-render everything inside the closest common ancestor with an `id` defined on it.\n\nTo get a better understanding, let's compare two applications that differ only by one `id` attribute.\n\n```js\nconst noId = () =\u003e render`\n  \u003cdiv id=\"root\"\u003e \u003c!-- The entire root will be re-rendered as it's the closest `id` to the changes --\u003e\n    \u003cspan\u003e\n      ${getState().count} \u003c!-- The actual changes --\u003e\n    \u003c/span\u003e\n    \u003cbutton\n      ::click=${({count}) =\u003e setState({count: count + 1})}\n    \u003e\n      Update\n    \u003c/button\u003e\n  \u003c/div\u003e\n`\n\nconst withId = () =\u003e render`\n  \u003cdiv id=\"root\"\u003e\n    \u003cspan id=\"count\"\u003e \u003c!-- Only this element will be re-rendered --\u003e\n      ${getState().count}\n    \u003c/span\u003e\n    \u003cbutton\n      ::click=${({count}) =\u003e setState({count: count + 1})}\n    \u003e\n      Update\n    \u003c/button\u003e\n  \u003c/div\u003e\n`\n```\n\nYou can see the difference in the graph below:\n\n```mermaid\ngraph TD\n  subgraph State\n    state[$count: 0 -\u003e 1 *]\n  end\n\n  subgraph withId\n    root2[#root] --\u003e span2[span#count] --\u003e count2[$count *] == rerender the nearest # ==\u003e span2\n    root2 --\u003e button2[button::click] == increment ==\u003e state\n  end\n\n  subgraph noId\n    root[#root] --\u003e span[span] --\u003e count[$count *] == rerender the nearest # ==\u003e root\n    root --\u003e button[button::click] == increment ==\u003e state\n  end\n```\n\nIn the _noId_ example, after updating the state inside the span, all the app gets re-rendered since the closest node with `id` is _root_.\nAs a consequence, _button_ loses focus.\nOn the other hand, in the _withId_ example, the only thing going to be re-rendered is text inside _span#count_.\n\n### Tips\n\n- Use uncontrolled text inputs and put them wisely, so they won't be re-rendered when the input value gets changed.\n  Form elements like checkboxes and selects could be used either in a controlled or uncontrolled way.\n- Wrap every component you want to be re-rendered independently with a tag with a unique `id`.\n- Do not rely on any constants declared in a component's scope inside event handlers.\n  Each event handler should be considered completely isolated from the upper scope.\n  The reason is that the Virtual DOM doesn't take into account any changes in event handlers.\n  Albeit you do may use a data-specific `id` on the tag to change this, it is not recommended due to performance reasons.\n  See the [example](https://tatomyr.github.io/purity/playground/#JYWwDg9gTgLgBAb2AO2DANHKBTZATbKAXzgDMoIQ4ByAOloHoUCAPWgKwGdqAoHgYwjJO8BCAgBXZBjgBzbDADKMAIYxsmTguVrsJALxwUaABQIA7hWSyAXHAAMmQVBz8Yd+0QCUfQcPgAgmBgcIYmXqEAfIg8cHB+InAA6gBKAPIAcgDioXLaqurhtJZCsrHxQokAwmkpKQCiVQAqufJKBdhFzq4wfHE4MBJQyFi4BFAABuVxADx4wABuRnj6AEQUEDCrkdNxcDNgOMtrJdbbSVa2cAAkCKmZWUQzDIfYO3t7MwBGEjAwQnBdh8bDZ+AAbYD8ADW+lu4SicC07V0Jnh+miZlOV3u2TgAGo4ABGbxeIhAuDvD57ACSyH4OBAuF6VP2DB+fyElI+MwAFlA4AwuZ9XsdVt1sG5tlVoD07LcanVGk0ni8cELZuz-iNyXEQeDITC4RF0Yj8iizOK3N4ERaZRL3BUXPb8USSWSWeqaXSGUzyc9NZzds95gtKVMyTxxFIYCYgmAfEA) for more context.\n\n- Root component must have the same `id` as the HTML element you want to mount the component to.\n  (Depends on the algorithm we're using for mounting.)\n- A **component**'s local state management is considered a secondary feature.\n  Therefore it's not a part of the library. However, it could possibly be implemented using the **rerender** method which is returned from the **init** function (see [example](./src/examples/use-state-example/StatefulCounter.js)).\n- The library doesn't sanitize your inputs.\n  Please do it by yourself or use the `sanitize.js` module.\n- Due to its asynchronous nature, **Purity** requires special testing for applications that use it.\n  Make sure you make delay(0) after the DOM has changed (see examples in [purity.test.ts](./src/purity.test.ts)).\n\n## Credits\n\nThis library is heavily inspired by project [innerself](https://github.com/stasm/innerself).\nAnd obviously, I was thinking of [React](https://github.com/facebook/react/).\n\nThe decision to use bare ES modules appears to be the consequence of listening to the brilliant Ryan Dahl's talk on [Deno](https://deno.land).\n\n## Examples of usage\n\n- [Counter](./src/examples/counter)\n- [Simple todo](./src/examples/simple-todo-example)\n- [Asynchronous todo](./src/examples/async-todo-example)\n- [Colored input](./src/examples/colored-input-example)\n- [Stateful counters](./src/examples/use-state-example)\n- [ToDo application](https://github.com/tatomyr/reactive-todo)\n- [Async search](./src/examples/async-search)\n- [Multiple Applications on the same page](./src/examples/multiple-apps)\n\nPlease find the examples [here](https://tatomyr.github.io/purity/examples/)\n\nIf you want to run them locally, see the [contributing guide](./CONTRIBUTING.md).\n\n## Playground\n\nFeel free to experiment in the [Playground](https://tatomyr.github.io/purity/playground/).\n\n## Miscellaneous\n\nThe library also includes a handful of algorithms from different sources, exported as ES modules to use with **Purity** or without.\n\nThe most important ones are `router` and `async` which could help with navigation and performing asynchronous operations respectively.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftatomyr%2Fpurity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftatomyr%2Fpurity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftatomyr%2Fpurity/lists"}