{"id":20631064,"url":"https://github.com/zsnout/willow","last_synced_at":"2025-10-05T12:41:54.757Z","repository":{"id":64691144,"uuid":"540050357","full_name":"zSnout/willow","owner":"zSnout","description":"Willow is a reactive web framework that enables JSX syntax, compiles to DOM nodes, and omits a virtual DOM.","archived":false,"fork":false,"pushed_at":"2025-08-13T02:41:59.000Z","size":496,"stargazers_count":3,"open_issues_count":6,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-12T05:52:33.969Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zSnout.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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},"funding":{"custom":["store.zsnout.com"]}},"created_at":"2022-09-22T15:34:26.000Z","updated_at":"2025-08-13T02:41:57.000Z","dependencies_parsed_at":"2024-01-24T00:27:05.814Z","dependency_job_id":"282cb7b2-ce10-4b87-a0c2-5d674aff1843","html_url":"https://github.com/zSnout/willow","commit_stats":{"total_commits":215,"total_committers":2,"mean_commits":107.5,"dds":"0.19999999999999996","last_synced_commit":"3cc1c1fa2fef6e2a607d98332bf62bffae623974"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zSnout/willow","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zSnout%2Fwillow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zSnout%2Fwillow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zSnout%2Fwillow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zSnout%2Fwillow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zSnout","download_url":"https://codeload.github.com/zSnout/willow/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zSnout%2Fwillow/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278457470,"owners_count":25989954,"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-05T02:00:06.059Z","response_time":54,"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":[],"created_at":"2024-11-16T14:10:43.617Z","updated_at":"2025-10-05T12:41:54.721Z","avatar_url":"https://github.com/zSnout.png","language":"TypeScript","funding_links":["store.zsnout.com"],"categories":[],"sub_categories":[],"readme":"# Welcome to Willow\n\nWhat is Willow? Willow\n\n- is a reactive library\n- enables JSX syntax\n- has static typing\n- makes DOM events easy\n- works with inputs fluidly\n- has simple components\n\nWillow does not aim to\n\n- use a virtual DOM\n- recompute components for every update (_cough_ _cough_ React)\n- be a custom compiler\n- give you every feature possible\n- create a custom language\n\n# What's unique about Willow?\n\nLike every web framework, Willow has a few unique features.\n\nFirst of all, it takes inspiration from SolidJS and copies its Signals, Effects,\nand Memos. However, it doesn't add extra features such as `batch` in order to\nstay as performant as possible.\n\nIn Willow, JSX compiles directly to DOM nodes. This allows you to use standard\nDOM operations with Willow elements, such as `.append()` and `.innerHTML`. This\nallows Willow components to be used in _almost every other web framework_.\n\nCompiling to DOM nodes has the alternate advantage of not requiring a custom\nrender function. How do you render a Willow component?\n`document.body.append(node)`.\n\nWillow also uses a custom element called a `WillowFragment` to render fragments.\nAt its core, a `WillowFragment` is basically a hidden element. We'll talk more\nabout it later. It you want to get to the juicy details, skip to\n[How do fragments work?](#how-do-fragments-work).\n\n# The reactivity system\n\nLike Solid, Willow's reactivity system is based on two primitives: Signals and\nEffects. A Signal stores a value and notifies linked Effects when it changes. An\nEffect can access Signals and runs code whenever its accessed Signals change.\nLet's look at how they work in code.\n\n## Signals\n\n```typescript\nimport { createSignal } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"Katniss\");\n\nconsole.log(name()); // Katniss\nsetName(\"Everdeen\");\nconsole.log(name()); // Everdeen\n```\n\nTo create a signal, we use `createSignal` and pass it an initial value. It\nreturns a tuple of two elements. The first is an accessor. When called, it\nreturns the current value of the Signal. The second is a setter function. When\ncalled with a value, it sets the current value of the signal to its argument and\nnotifies Effects about the change.\n\nYou can also pass a Signal a function taking the previous value and returning\nthe new one. This can be used to prevent Effects from tracking that Signal.\n\n```typescript\nimport { createSignal } from \"@zsnout/willow\";\n\nconst [age, setAge] = createSignal(13);\n\nconsole.log(age()); // 13\nsetAge((oldAge) =\u003e oldAge + 1);\nconsole.log(age()); // 14\n```\n\nFor TypeScript users, here are the type declarations for Signals:\n\n```typescript\ntype Accessor\u003cT\u003e = () =\u003e T;\ntype Setter\u003cT\u003e = (value: T) =\u003e void;\ntype Updater\u003cT\u003e = (update: (oldValue: T) =\u003e T) =\u003e void;\ntype SetterAndUpdater\u003cT\u003e = Setter\u003cT\u003e \u0026 Updater\u003cT\u003e;\ntype Signal\u003cT\u003e = [get: Accessor\u003cT\u003e, set: SetterAndUpdater\u003cT\u003e];\n\nfunction createSignal\u003cT = any\u003e(): Signal\u003cT | undefined\u003e;\nfunction createSignal\u003cT\u003e(value: T): Signal\u003cT\u003e;\n```\n\n## Effects\n\nSpeaking of Effects, let's learn how to use them. We'll update our previous\nexample to use Effects instead of manual checking.\n\n```typescript\nimport { createEffect, createSignal } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"Harry\");\n\ncreateEffect(() =\u003e {\n  console.log(name());\n});\n// The effect is run once and outputs Harry.\n\nsetName(\"Potter\");\n// The effect automatically reruns and outputs Potter.\n\nsetName(\"\");\n// The effect automatically reruns and outputs no text.\n```\n\nTo create an effect, we use `createEffect` and pass a function. The function is\nimmediately run once and checked for accessed Signals. Whenever these Signals\nare changed, the effect is updated synchronously.\n\n## Memos\n\nMemos are a combination of Signals and Effects. They compute a value and update\nit whenever its dependencies change.\n\n```typescript\nimport { createMemo, createSignal } from \"@zsnout/willow\";\n\nconst [number, setNumber] = createSignal(4);\nconst doubled = createMemo(() =\u003e number() * 2);\n\nconsole.log(number()); // 4\nconsole.log(doubled()); // 8\n\nsetNumber(7);\nconsole.log(number()); // 7\nconsole.log(doubled()); // 14\n```\n\nIn Willow, JSX components use Effects under the hood to update whenever values\nchange. To Willow, rendering is just a side effect of the reactivity system.\n\n# Let's write some JSX\n\nNow that we understand reactivity, let's use it to write some JSX code. We'll\nstart by creating a fragment. For those who haven't used them before, a fragment\nbasically holds a bunch of DOM nodes. When appended to the DOM, they are\nrendered without a container element. Willow's fragments are implemented in a\nspecial way, but we'll talk about them later. To write a fragment in JSX, write\nan empty HTML tag. That's it!\n\nWhen writing JSX code, you'll need to import Willow's `h` function. This is used\nbehind the scenes to render JSX.\n\n```tsx\nimport { h } from \"@zsnout/willow\";\n\nconst root = \u003c\u003e\u003c/\u003e;\n```\n\nLet's add an HTML element into this that shows the person's name. We'll start by\ncreating a Signal for their name.\n\n```tsx\nimport { createSignal, h } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nconst root = (\n  \u003c\u003e\n    \u003cp\u003eYour name is {name}.\u003c/p\u003e\n  \u003c/\u003e\n);\n```\n\nNotice how we're not using an Effect to re-render the paragraph when the name\nchanges. Willow detects that we're passing a Signal and automatically creates an\nEffect around it.\n\n### Rendering into the DOM\n\nTo render our script into the DOM, we'll use a standard DOM method.\n\n```tsx\nimport { createSignal, h } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nconst root = (\n  \u003c\u003e\n    \u003cp\u003eYour name is {name}.\u003c/p\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\n### The event system\n\nLet's add an input field and learn how Willow's event system works.\n\n```tsx\nimport { createSignal, h } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nconst root = (\n  \u003c\u003e\n    \u003cinput value={name} on:input={(event) =\u003e setName(event.target.value)} /\u003e\n    \u003cp\u003eYour name is {name}.\u003c/p\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\nNotice how we used curly braces to pass JavaScript expressions to JSX\nattributes. This is a common pattern and one you'll see a lot, so make sure to\nremember it. Additionally, most JSX attributes accept Signals or direct values.\n\nWe also used a `/\u003e` to close the `input` element. While not required in HTML\ncode, explicitly closing an element is required by JSX law, so make sure to add\nit.\n\nYou'll also see how Willow uses `on:event` methods to bind event handlers. Most\nframework use `onEvent` for native events and `on:event` for custom events, but\nWillow simplifies this by using the same syntax for both. You'll also notice\nthat we used ES6 arrow functions to capture the event parameter and call\n`setName`.\n\n### bind:... syntax\n\nThis looks like a lot of boilerplate just to work with input fields. Is there an\neasier syntax? Of course! Willow provides a few builtin bind:... attributes, and\none of those is bind:value. It accepts a Signal and automatically binds to the\n`value` attribute and `on:input` event. Let's use it.\n\n```tsx\nimport { createSignal, h } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nconst root = (\n  \u003c\u003e\n    \u003cinput bind:value={[name, setName]} /\u003e\n    \u003cp\u003eYour name is {name}.\u003c/p\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\nWe've now created a simple form for users to type in their name before we greet\nthem.\n\n### Using \u0026lt;Maybe\u0026gt;\n\nSomething seems off about the demo. Maybe it's that we say \"You name is .\" when\nthe input field is empty. Let's fix that by using our first JSX component:\n`\u003cMaybe\u003e`.\n\n`\u003cMaybe\u003e` accepts a `when` prop. It should be an accessor that returns a\nboolean. In Willow, an accessor is either\n\n1. the first part of a Signal, also known as the getter,\n2. a Memo, or\n3. a function accepting zero arguments and returning a value.\n\nWe'll use the third option in our `\u003cMaybe\u003e` and only show the paragraph when the\nuser's name is over 3 letters long.\n\n```tsx\nimport { createSignal, h, Maybe } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nconst root = (\n  \u003c\u003e\n    \u003cinput bind:value={[name, setName]} /\u003e\n\n    \u003cMaybe when={() =\u003e name().length \u003e= 3}\u003e\n      \u003cp\u003eYour name is {name}.\u003c/p\u003e\n    \u003c/Maybe\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\nCongratulations! You've just used your first JSX component. Let's create one by\nextracting the `\u003cMaybe\u003e` logic into its own component.\n\n### Creating components\n\nA component is just a function that returns a DOM node or JSX content (but JSX\nis just shorthand for DOM nodes). A component accepts one parameter, its props\n(short for properties). We'll make a component called `ConditionalName` that\naccepts an accessor for its `name`.\n\nIn the example below, we use destructuring to get the name prop from the first\nargument.\n\n```tsx\nimport { createSignal, h, Maybe } from \"@zsnout/willow\";\n\nconst [name, setName] = createSignal(\"\");\n\nfunction ConditionalName({ name }) {\n  return (\n    \u003cMaybe when={() =\u003e name().length \u003e= 3}\u003e\n      \u003cp\u003eYour name is {name}.\u003c/p\u003e\n    \u003c/Maybe\u003e\n  );\n}\n\nconst root = (\n  \u003c\u003e\n    \u003cinput bind:value={[name, setName]} /\u003e\n    \u003cConditionalName name={name} /\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\nWe can even extract the whole fragment into a component.\n\n```tsx\nimport { createSignal, h, Maybe } from \"@zsnout/willow\";\n\nfunction NameInput() {\n  const [name, setName] = createSignal(\"\");\n\n  return (\n    \u003c\u003e\n      \u003cinput bind:value={[name, setName]} /\u003e\n      \u003cConditionalName name={name} /\u003e\n    \u003c/\u003e\n  );\n}\n\nfunction ConditionalName({ name }) {\n  return (\n    \u003cMaybe when={() =\u003e name().length \u003e= 3}\u003e\n      \u003cp\u003eYour name is {name}.\u003c/p\u003e\n    \u003c/Maybe\u003e\n  );\n}\n\nconst root = \u003cNameInput /\u003e;\n\ndocument.body.append(root);\n```\n\nNow that all the code is in separate components, we can use it multiple times.\n\n```tsx\nimport { createSignal, h, Maybe } from \"@zsnout/willow\";\n\nfunction NameInput() {\n  const [name, setName] = createSignal(\"\");\n\n  return (\n    \u003c\u003e\n      \u003cinput bind:value={[name, setName]} /\u003e\n      \u003cConditionalName name={name} /\u003e\n    \u003c/\u003e\n  );\n}\n\nfunction ConditionalName({ name }) {\n  return (\n    \u003cMaybe when={() =\u003e name().length \u003e= 3}\u003e\n      \u003cp\u003eYour name is {name}.\u003c/p\u003e\n    \u003c/Maybe\u003e\n  );\n}\n\nconst root = (\n  \u003c\u003e\n    \u003cp\u003eThe first account\u003c/p\u003e\n    \u003cNameInput /\u003e\n\n    \u003cp\u003eThe second user\u003c/p\u003e\n    \u003cNameInput /\u003e\n\n    \u003cp\u003eThe third wheel\u003c/p\u003e\n    \u003cNameInput /\u003e\n  \u003c/\u003e\n);\n\ndocument.body.append(root);\n```\n\n# How do fragments work?\n\nOkay, let's talk about Willow's fragments now. They're implemented in a very\nunusual way, but it's very clever and works without complex reactive systems.\n\nWhen first designing fragments, the Willow team used native `DocumentFragment`s,\nand they seemed pretty good. Unfortunately, `DocumentFragment`s lose their\nchildren when appended to the DOM, so they weren't an optimal choice. Our team\ndecided to recreate these using our own logic.\n\nWe created a `WillowFragment` class that extended a DOM `Comment`. The comment\nwas to be used as an anchor to which the fragment's children would be appended.\nWe then created custom getters, setters, and methods for these DOM methods:\nafter, appendChild, before, children, childNodes, contains, firstChild,\nhasChildNodes, insertBefore, lastChild, nextElementSibling, nextSibling, remove,\nremoveChild, replaceChild, and replaceWith. We then use the DOMNodeInserted and\nDOMNodeRemoved events to detect when the element is appended as a child or\nremoved and append its virtual children after the comment.\n\nThe reason we use `WillowFragment`s is that they can be passed around, inserted,\nremoved, and act like normal DOM nodes. The cost of this amazing addition is a\nmere 1.4 kilobytes and makes things easier on any developer working on Willow\nprojects.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzsnout%2Fwillow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzsnout%2Fwillow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzsnout%2Fwillow/lists"}