{"id":21891945,"url":"https://github.com/capsidjs/capsule","last_synced_at":"2025-04-15T14:28:57.194Z","repository":{"id":43778783,"uuid":"445586469","full_name":"capsidjs/capsule","owner":"capsidjs","description":"💊 UI component as a bundle of local event handlers. See also https://github.com/kt3k/cell, which is the successor project","archived":false,"fork":false,"pushed_at":"2024-06-17T15:29:54.000Z","size":137,"stargazers_count":28,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-11T03:21:27.641Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://capsule.deno.dev/","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/capsidjs.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}},"created_at":"2022-01-07T16:37:38.000Z","updated_at":"2025-01-31T03:04:14.000Z","dependencies_parsed_at":"2024-04-22T04:24:31.980Z","dependency_job_id":"1c70afdd-28dc-4319-a7d9-5a2c1b45f259","html_url":"https://github.com/capsidjs/capsule","commit_stats":{"total_commits":67,"total_committers":1,"mean_commits":67.0,"dds":0.0,"last_synced_commit":"53120627b211aefbab2715263bffe4711b26bec0"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/capsidjs%2Fcapsule","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/capsidjs%2Fcapsule/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/capsidjs%2Fcapsule/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/capsidjs%2Fcapsule/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/capsidjs","download_url":"https://codeload.github.com/capsidjs/capsule/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249088154,"owners_count":21210758,"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":[],"created_at":"2024-11-28T12:46:12.151Z","updated_at":"2025-04-15T14:28:57.168Z","avatar_url":"https://github.com/capsidjs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://raw.githubusercontent.com/capsidjs/capsule/master/capsule-logo.svg\" width=\"70\" alt=\"capsule\" /\u003e\n\n# Capsule v0.6.1\n\n[![ci](https://github.com/capsidjs/capsule/actions/workflows/ci.yml/badge.svg)](https://github.com/capsidjs/capsule/actions/workflows/ci.yml)\n\n\u003e Event-driven DOM programming in a new style\n\n# Features\n\n- Supports **event-driven** style of frontend programming in a **new way**.\n- Supports **event delegation** and **outside events** out of the box.\n- **Lightweight** library.\n  [**1.25 kiB**](https://raw.githubusercontent.com/capsidjs/capsule/v0.6.1/dist.min.js)\n  gzipped. **No dependencies**. **No build** steps.\n- Uses **plain JavaScript** and **plain HTML**, requires **No special syntax**.\n- **TypeScript** friendly.\n\n[See live examples](https://capsule.deno.dev/)\n\n# Motivation\n\nVirtual DOM frameworks are good for many use cases, but sometimes they are\noverkill for the use cases where you only need a little bit of event handlers\nand dom modifications.\n\nThis `capsule` library explores the new way of simple event-driven DOM\nprogramming without virtual dom.\n\n# Slogans\n\n- Local query is good. Global query is bad.\n- Define behaviors based on HTML classes.\n- Use pubsub when making remote effect.\n\n## Local query is good. Global query is bad\n\nWhen people use jQuery, they often do:\n\n```js\n$(\".some-class\").each(function () {\n  $(this).on(\"some-event\", () =\u003e {\n    $(\".some-target\").each(function () {\n      // some effects on this element\n    });\n  });\n});\n```\n\nThis is very common pattern, and this is very bad.\n\nThe above code can been seen as a behavior of `.some-class` elements, and they\nuse global query `$(\".some-target\")`. Because they use global query here, they\ndepend on the entire DOM tree of the page. If the page change anything in it,\nthe behavior of the above code can potentially be changed.\n\nThis is so unpredictable because any change in the page can affect the behavior\nof the above class. You can predict what happens with the above code only when\nyou understand every details of the entire application, and that's often\nimpossible when the application is large size, and multiple people working on\nthat app.\n\nSo how to fix this? We recommend you should use **local** queries.\n\nLet's see this example:\n\n```js\n$(\".some-class\").each(function () {\n  $(this).on(\"some-event\", () =\u003e {\n    $(this).find(\".some-target\").each(function () {\n      // some effects on this element\n    });\n  });\n});\n```\n\nThe difference is `$(this).find(\".some-target\")` part. This selects the elements\nonly under each `.some-class` element. So this code only depends on the elements\ninside it, which means there is no global dependencies here.\n\n`capsule` enforces this pattern by providing `query` function to event handlers\nwhich only finds elements under the given element.\n\n```js\nconst { on } = component(\"some-class\");\n\non.click = ({ query }) =\u003e {\n  query(\".some-target\").textContent = \"clicked\";\n};\n```\n\nHere `query` is the alias of `el.querySelector` and it finds `.some-target` only\nunder it. So the dependency is **local** here.\n\n## Define behaviors based on HTML classes\n\nFrom our observation, skilled jQuery developers always define DOM behaviors\nbased on HTML classes.\n\nWe borrowed this pattern, and `capsule` allows you to define behavior only based\non HTML classes, not random combination of query selectors.\n\n```html\n\u003cdiv class=\"hello\"\u003eJohn Doe\u003c/div\u003e\n```\n\n```js\nconst { on } = component(\"hello\");\n\non.__mount__ = () =\u003e {\n  alert(`Hello, I'm ${el.textContext}!`); // Alerts \"Hello, I'm John Doe!\"\n};\n```\n\n## Use pubsub when making remote effect\n\nWe generally recommend using only local queries, but how to make effects to the\nremote elements?\n\nWe reommend using pubsub pattern here. By using this pattern, you can decouple\nthose affecting and affected elements. If you decouple those elements, you can\ntest those components independently by using events as I/O of those components.\n\n`capsule` library provides `pub` and `sub` APIs for encouraging this pattern.\n\n```js\nconst EVENT = \"my-event\";\n{\n  const { on } = component(\"publisher\");\n\n  on.click = ({ pub }) =\u003e {\n    pub(EVENT);\n  };\n}\n\n{\n  const { on, sub } = component(\"subscriber\");\n\n  sub(EVENT);\n\n  on[EVENT] = () =\u003e {\n    alert(`Got ${EVENT}!`);\n  };\n}\n```\n\nNote: `capsule` uses DOM Event as event payload, and `sub:EVENT` HTML class as\nregistration to the event. When `pub(EVENT)` is called the CustomEvent of\n`EVENT` type are dispatched to the elements which have `sub:EVENT` class.\n\n## TodoMVC\n\nTodoMVC implementation is also available\n[here](https://github.com/capsidjs/capsule-todomvc).\n\n## Live examples\n\nSee [the live demos](https://capsule.deno.dev/).\n\n# Install\n\nVanilla js (ES Module):\n\n```html\n\u003cscript type=\"module\"\u003e\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n// ... your code ...\n\u003c/script\u003e\n```\n\nVanilla js (Legacy script tag):\n\n```html\n\u003cscript src=\"https://deno.land/x/capsule@v0.6.1/loader.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\ncapsuleLoader.then((capsule) =\u003e {\n  const { component } = capsule;\n  // ... your code ...\n});\n\u003c/script\u003e\n```\n\nDeno:\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/mod.ts\";\n```\n\nVia npm:\n\n```\nnpm install @kt3k/capsule\n```\n\nand\n\n```js\nimport { component } from \"@kt3k/capsule\";\n```\n\n# Examples\n\nMirrors input value of `\u003cinput\u003e` element to another dom.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst { on } = component(\"mirroring\");\n\non.input = ({ query }) =\u003e {\n  query(\".src\").textContent = query(\".dest\").value;\n};\n```\n\nPubsub.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst EVENT = \"my-event\";\n\n{\n  const { on } = component(\"pub-element\");\n\n  on.click = ({ pub }) =\u003e {\n    pub(EVENT, { hello: \"world!\" });\n  };\n}\n\n{\n  const { on, sub } = component(\"sub-element\");\n\n  sub(EVENT);\n\n  on[EVENT] = ({ e }) =\u003e {\n    console.log(e.detail.hello); // =\u003e world!\n  };\n}\n```\n\nMount hooks.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst { on } = component(\"my-component\");\n\n// __mount__ handler is called when the component mounts to the elements.\non.__mount__ = () =\u003e {\n  console.log(\"hello, I'm mounted\");\n};\n```\n\nPrevent default, stop propagation.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst { on } = component(\"my-component\");\n\non.click = ({ e }) =\u003e {\n  // e is the native event object.\n  // You can call methods of Event object\n  e.stopPropagation();\n  e.preventDefault();\n  console.log(\"hello, I'm mounted\");\n};\n```\n\nEvent delegation. You can assign handlers to `on(selector).event` to use\n[event delegation](https://www.geeksforgeeks.org/event-delegation-in-javascript/)\npattern.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst { on } = component(\"my-component\");\n\non(\".btn\").click = ({ e }) =\u003e {\n  console.log(\".btn is clicked!\");\n};\n```\n\nOutside event handler. By assigning `on.outside.event`, you can handle the event\noutside of the component dom.\n\n```js\nimport { component } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n\nconst { on } = component(\"my-component\");\n\non.outside.click = ({ e }) =\u003e {\n  console.log(\"The outside of my-component has been clicked!\");\n};\n```\n\n# API reference\n\n```ts\nconst { component, mount } from \"https://deno.land/x/capsule@v0.6.1/dist.min.js\";\n```\n\n## `component(name): ComponentResult`\n\nThis registers the component of the given name. This returns a `ComponentResult`\nwhich has the following shape.\n\n```ts\ninterface ComponentResult {\n  on: EventRegistryProxy;\n  is(name: string);\n  sub(type: string);\n  innerHTML(html: string);\n}\n\ninterface EventRegistry {\n  [key: string]: EventHandler | {};\n  (selector: string): {\n    [key: string]: EventHandler;\n  };\n  outside: {\n    [key: string]: EventHandler;\n  };\n}\n```\n\n## `component().on[eventName] = EventHandler`\n\nYou can register event handler by assigning to `on.event`.\n\n```ts\nconst { on } = component(\"my-component\");\n\non.click = () =\u003e {\n  alert(\"clicked\");\n};\n```\n\n## `component().on(selector)[eventName] = EventHandler`\n\nYou can register event handler by assigning to `on(selector).event`.\n\nThe actual event handler is attached to the component dom (the root of element\nwhich this component mounts), but the handler is only triggered when the target\nis inside the given `selector`.\n\n```ts\nconst { on } = component(\"my-component\");\n\non(\".btn\").click = () =\u003e {\n  alert(\".btn is clicked\");\n};\n```\n\n## `component().on.outside[eventName] = EventHandler`\n\nYou can register event handler for the outside of the component dom by assigning\nto `on.outside.event`\n\n```ts\nconst { on } = component(\"my-component\");\n\non.outside.click = () =\u003e {\n  console.log(\"outside of the component has been clicked!\");\n};\n```\n\nThis is useful for implementing a tooltip which closes itself if the outside of\nit is clicked.\n\n## `component().is(name: string)`\n\n`is(name)` sets the html class to the component dom at `mount` phase.\n\n```ts\nconst { is } = component(\"my-component\");\n\nis(\"my-class-name\");\n```\n\n## `component().innerHTML(html: string)`\n\n`innerHTML(html)` sets the inner html to the component dom at `mount` phase.\n\n```ts\nconst { innerHTML } = component(\"my-component\");\n\ninnerHTML(\"\u003ch1\u003eGreetings!\u003c/h1\u003e\u003cp\u003eHello from my-component\u003c/p\u003e\");\n```\n\n## `component().sub(type: string)`\n\n`sub(type)` sets the html class of the form `sub:type` to the component at\n`mount` phase. By adding `sub:type` class, the component can receive the event\nfrom `pub(type)` calls.\n\n```ts\n{\n  const { sub, on } = component(\"my-component\");\n  sub(\"my-event\");\n  on[\"my-event\"] = () =\u003e {\n    alert(\"Got my-event\");\n  };\n}\n{\n  const { on } = component(\"another-component\");\n  on.click = ({ pub }) =\u003e {\n    pub(\"my-event\");\n  };\n}\n```\n\n## `EventHandler`\n\nThe event handler in `capsule` has the following signature. The first argument\nis `EventHandlerContext`, not `Event`.\n\n```ts\ntype EventHandler = (ctx: ComponentEventContext) =\u003e void;\n```\n\n## `ComponentEventContext`\n\n```ts\ninterface ComponentEventContext {\n  e: Event;\n  el: Element;\n  pub\u003cT = unknown\u003e(name: string, data: T): void;\n  query(selector: string): Element | null;\n  queryAll(selector: string): NodeListOf\u003cElement\u003e | null;\n}\n```\n\n`e` is the native DOM Event. You can call APIs like `.preventDefault()` or\n`.stopPropagation()` via this object.\n\n`el` is the DOM Element, which the event handler is bound to, and the event is\ndispatched on.\n\nYou can optionally attach data to the event. The attached data is available via\n`.detail` property of `CustomEvent` object.\n\n`pub(type)` dispatches the event to the remote elements which have `sub:type`\nclass. This should be used with `sub(type)` calls. For example:\n\n```ts\n{\n  const { sub, on } = component(\"my-component\");\n  sub(\"my-event\");\n  on[\"my-event\"] = () =\u003e {\n    alert(\"Got my-event\");\n  };\n}\n{\n  const { on } = component(\"another-component\");\n  on.click = ({ pub }) =\u003e {\n    pub(\"my-event\");\n  };\n}\n```\n\nThis call dispatches `new CustomEvent(\"my-type\")` to the elements which have\n`sub:my-type` class, like `\u003cdiv class=\"sub:my-type\"\u003e\u003c/div\u003e`. The event doesn't\nbubbles up.\n\nThis method is for communicating with the remote elements which aren't in\nparent-child relationship.\n\n## `mount(name?: string, el?: Element)`\n\nThis function initializes the elements with the given configuration. `component`\ncall itself initializes the component of the given class name automatically when\ndocument got ready, but if elements are added after the initial page load, you\nneed to call this method explicitly to initialize capsule's event handlers.\n\n```js\n// Initializes the all components in the entire page.\nmount();\n\n// Initializes only \"my-component\" components in the entire page.\n// You can use this when you only added \"my-component\" component.\nmount(\"my-compnent\");\n\n// Initializes the all components only in `myDom` element.\n// You can use this when you only added something under `myDom`.\nmount(undefined, myDom);\n\n// Initializes only \"my-component\" components only in `myDom` element.\n// You can use this when you only added \"my-component\" under `myDom`.\nmount(\"my-component\", myDom);\n```\n\n## `unmount(name: string, el: Element)`\n\nThis function unmounts the component of the given name from the element. This\nremoves the all event listeners of the component and also calls the\n`__unmount__` hooks.\n\n```js\nconst { on } = component(\"my-component\");\n\non.__unmount__ = () =\u003e {\n  console.log(\"unmounting!\");\n};\n\nunmount(\"my-component\", el);\n```\n\nNote: It's ok to just remove the mounted elements without calling `unmount`.\nSuch removals don't cause a problem in most cases, but if you use `outside`\nhandlers, you need to call unmount to prevent the leakage of the event handler\nbecause outside handlers are bound to `document` object.\n\n# How `capsule` works\n\nThis section describes how `capsule` works in a big picture.\n\nLet's look at the below basic example.\n\n```js\nconst { on } = component(\"my-component\");\n\non.click = () =\u003e {\n  console.log(\"clicked\");\n};\n```\n\nThis code is roughly translated into jQuery like the below:\n\n```js\n$(document).read(() =\u003e {\n  $(\".my-component\").each(function () {\n    $this = $(this);\n\n    if (isAlreadyInitialized($this)) {\n      return;\n    }\n\n    $this.click(() =\u003e {\n      console.log(\"clicked\");\n    });\n  });\n});\n```\n\n`capsule` can be seen as a syntax sugar for the above pattern (with a few more\nutilities).\n\n# Prior art\n\n- [capsid](https://github.com/capsidjs/capsid)\n  - `capsule` is heavily inspired by `capsid`\n\n# Projects with similar concepts\n\n- [Flight](https://flightjs.github.io/) by twitter\n  - Not under active development\n- [eddy.js](https://github.com/WebReflection/eddy)\n  - Archived\n\n# History\n\n- 2022-01-13 v0.5.2 Change `el` typing. #2\n- 2022-01-12 v0.5.1 Fix `__mount__` hook execution order\n- 2022-01-12 v0.5.0 Add tests, setup CI.\n- 2022-01-11 v0.4.0 Add outside handlers.\n- 2022-01-11 v0.3.0 Add `unmount`.\n- 2022-01-11 v0.2.0 Change delegation syntax.\n\n# License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapsidjs%2Fcapsule","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcapsidjs%2Fcapsule","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcapsidjs%2Fcapsule/lists"}