{"id":29052811,"url":"https://github.com/smnandre/stimulus-delegation","last_synced_at":"2025-06-27T00:38:30.976Z","repository":{"id":298935212,"uuid":"1000818325","full_name":"smnandre/stimulus-delegation","owner":"smnandre","description":"A Typescript mixin for Stimulus controllers providing event delegation with automatic cleanup","archived":false,"fork":false,"pushed_at":"2025-06-25T23:17:41.000Z","size":66,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-26T00:27:33.305Z","etag":null,"topics":["dom-event","event-delegation","performances","stimulus","stimulus-controller","stimulus-mixin","symfony-ux","typescript"],"latest_commit_sha":null,"homepage":"","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/smnandre.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"smnandre"}},"created_at":"2025-06-12T11:18:08.000Z","updated_at":"2025-06-25T23:17:45.000Z","dependencies_parsed_at":"2025-06-13T18:02:24.641Z","dependency_job_id":"81eb6bb1-d61e-4332-833b-2b50230a12af","html_url":"https://github.com/smnandre/stimulus-delegation","commit_stats":null,"previous_names":["smnandre/stimulus-delegation"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/smnandre/stimulus-delegation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smnandre%2Fstimulus-delegation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smnandre%2Fstimulus-delegation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smnandre%2Fstimulus-delegation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smnandre%2Fstimulus-delegation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smnandre","download_url":"https://codeload.github.com/smnandre/stimulus-delegation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smnandre%2Fstimulus-delegation/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261978383,"owners_count":23239381,"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-event","event-delegation","performances","stimulus","stimulus-controller","stimulus-mixin","symfony-ux","typescript"],"created_at":"2025-06-27T00:38:30.146Z","updated_at":"2025-06-27T00:38:30.961Z","avatar_url":"https://github.com/smnandre.png","language":"TypeScript","funding_links":["https://github.com/sponsors/smnandre"],"categories":[],"sub_categories":[],"readme":"# Stimulus Delegation: `useDelegation`\n\nA function for Stimulus controllers that provides efficient event delegation capabilities. Handle events on dynamically\nadded elements and nested structures without manual event listener management.\n\n\u003e [!TIP]\n\u003e This function helps you wire up DOM event delegation in Stimulus controllers, both declaratively and imperatively,\n\u003e without the need for additional build steps or decorators.\n\nIf you can, please consider [sponsoring](https://github.com/sponsors/smnandre) this project to support its development\nand maintenance.\n\n## Features\n\n- **Event Delegation**: Listen to events on child elements using CSS selectors\n- **Dynamic Content Support**: Automatically handles dynamically added elements\n- **Automatic Cleanup**: Proper memory management with lifecycle-aware cleanup\n- **TypeScript Support**: Fully typed with proper interfaces and generics\n- **Method Chaining**: Fluent API for setting up multiple delegations\n- **Performance Optimized**: Uses event bubbling and `closest()` for efficient matching\n\n## Installation\n\n### Using npm\n\n```bash\nnpm install @smnandre/stimulus-delegation\n```\n\n### Using JSDeliver\n\nIf you prefer to use a CDN, you can import it directly from JSDeliver:\n\n```js\nimport {useDelegation} from 'https://cdn.jsdelivr.net/npm/@smnandre/stimulus-delegation@latest';\n```\n\n## Basic Usage\n\n```typescript\nimport {Controller} from '@hotwired/stimulus';\nimport {useDelegation} from '@smnandre/stimulus-delegation';\n\nexport default class extends Controller {\n  initialize() {\n    // Pass the controller instance directly to the function\n    useDelegation(this);\n  }\n\n  connect() {\n    // Still use the fluent API for setting up delegations\n    this.delegate('click', '.btn[data-action]', this.handleButtonClick)\n      .delegate('input', 'input[type=\"text\"]', this.handleTextInput);\n  }\n\n  handleButtonClick(event, target) {\n    console.log(`Button clicked: ${target.dataset.action}`);\n  }\n\n  handleTextInput(event, target) {\n    console.log(`Input value: ${target.value}`);\n  }\n}\n```\n\n## API Reference\n\n### Methods\n\n#### `delegate(eventType, selector, handler)`\n\nSets up event delegation for the specified event type and CSS selector.\n\n- **eventType**: `string` - The event type to listen for (e.g., 'click', 'input')\n- **selector**: `string` - CSS selector to match target elements\n- **handler**: `DelegationHandler` - Function to call when event occurs\n- **Returns**: `this` - For method chaining\n\n```typescript\nthis.delegate('click', '.delete-btn', this.handleDelete);\n```\n\n#### `undelegate(eventType, selector)`\n\nRemoves a specific delegated event listener.\n\n- **eventType**: `string` - The event type\n- **selector**: `string` - CSS selector that was used\n- **Returns**: `this` - For method chaining\n\n```typescript\nthis.undelegate('click', '.delete-btn');\n```\n\n#### `undelegateAll()`\n\nRemoves all delegated event listeners. This is handled automatically when the controller disconnects, so you don't need to call it manually unless you want to remove all delegations before disconnect.\n\n- **Returns**: `this` - For method chaining\n\n## Advanced Usage\n\n### Complex Selectors\n\nUse any valid CSS selector for precise targeting:\n\n```typescript\n// Attribute selectors\nthis.delegate('click', '[data-action=\"save\"]', this.handleSave);\n\n// Class combinations\nthis.delegate('click', '.btn.primary:not(.disabled)', this.handlePrimary);\n\n// Descendant selectors\nthis.delegate('change', 'form .required-field', this.handleRequired);\n\n// Multiple selectors (use separate calls)\nthis.delegate('click', '.edit-btn', this.handleEdit);\nthis.delegate('click', '.delete-btn', this.handleDelete);\n```\n\n### Nested Elements\n\nThe delegation mechanism uses `element.closest(selector)` to find matching ancestors:\n\n```html\n// HTML\n\u003cdiv class=\"card\" data-id=\"123\"\u003e\n    \u003ch3\u003eCard Title\u003c/h3\u003e\n    \u003cspan class=\"clickable\"\u003eClick anywhere in card\u003c/span\u003e\n    \u003cdiv class=\"actions\"\u003e\n        \u003cbutton\u003eEdit\u003c/button\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\n```typescript\n// Controller\nthis.delegate('click', '.card', this.handleCardClick);\n\n// Handler function\nfunction handleCardClick(event, target) {\n  // target will be the .card element even if you click the span or button\n  const cardId = target.dataset.id;\n  console.log(`Card ${cardId} clicked`);\n}\n```\n\n### Dynamic Content\n\nDelegation automatically works with dynamically added elements:\n\n```javascript\n// Connect method\nfunction connect() {\n  this.delegate('click', '.dynamic-btn', this.handleDynamic);\n}\n\n// Add new button method\nfunction addNewButton() {\n  const button = document.createElement('button');\n  button.className = 'dynamic-btn';\n  button.textContent = 'New Button';\n  this.element.appendChild(button);\n  // Event delegation automatically works!\n}\n```\n\n### Event Handler Context\n\nHandlers are bound to the controller instance:\n\n```typescript\n// Handler function\nfunction handleClick(event, target) {\n  // `this` refers to the controller\n  this.someMethod();\n  console.log(this.element); // Controller's element\n\n  // Access the event and matched target\n  event.preventDefault();\n  const buttonText = target.textContent;\n}\n```\n\n## TypeScript Integration\n\nTo ensure type safety in your TypeScript project, you can inform the compiler that your controller has been enhanced with delegation capabilities.\n\nDeclare the delegation methods on your controller class and TypeScript will recognize them.\n\n```typescript\nimport { Controller } from '@hotwired/stimulus'\nimport { useDelegation, DelegationController } from '@smnandre/stimulus-delegation'\n\nexport default class extends Controller {\n  // Inform TypeScript about the added methods\n  delegate!: DelegationController['delegate']\n  undelegate!: DelegationController['undelegate']\n  undelegateAll!: DelegationController['undelegateAll']\n\n  initialize() {\n    useDelegation(this)\n  }\n\n  connect() {\n    this.delegate('click', '.btn', this.handleClick)\n  }\n\n  handleClick(event: Event, target: Element) {\n    // handler logic\n  }\n}\n```\n\n## Real-World Examples\n\n### Todo List Controller\n\n```typescript\nexport default class extends Controller {\n  initialize() {\n    useDelegation(this)\n  }\n\n  connect() {\n    this.delegate('click', '.todo-toggle', this.toggleTodo)\n      .delegate('click', '.todo-delete', this.deleteTodo)\n      .delegate('dblclick', '.todo-label', this.editTodo)\n      .delegate('keypress', '.todo-edit', this.saveEdit)\n      .delegate('blur', '.todo-edit', this.cancelEdit);\n  }\n\n  toggleTodo(event: Event, target: Element) {\n    const checkbox = target as HTMLInputElement;\n    const todoItem = checkbox.closest('.todo-item');\n    todoItem?.classList.toggle('completed', checkbox.checked);\n  }\n\n  deleteTodo(event: Event, target: Element) {\n    const todoItem = target.closest('.todo-item');\n    todoItem?.remove();\n  }\n}\n```\n\n### Data Table Controller\n\n```typescript\nexport default class extends Controller {\n  initialize() {\n    useDelegation(this)\n  }\n\n  connect() {\n    this.delegate('click', 'th[data-sortable]', this.handleSort)\n      .delegate('click', '.pagination-btn', this.handlePagination)\n      .delegate('change', '.row-checkbox', this.handleRowSelect)\n      .delegate('click', '.action-btn', this.handleRowAction);\n  }\n\n  handleSort(event: Event, target: Element) {\n    const column = (target as HTMLElement).dataset.column;\n    // Sort logic here\n  }\n\n  handleRowAction(event: Event, target: Element) {\n    const action = (target as HTMLElement).dataset.action;\n    const row = target.closest('tr');\n    const rowId = row?.dataset.id;\n\n    switch (action) {\n      case 'edit':\n        this.editRow(rowId);\n        break;\n      case 'delete':\n        this.deleteRow(rowId);\n        break;\n    }\n  }\n}\n```\n\n## Testing\n\n### Unit Tests\n\n```typescript\nimport { describe, it, expect, vi } from 'vitest'\nimport { useDelegation } from '@smnandre/stimulus-delegation'\nimport { Controller } from '@hotwired/stimulus'\n\ndescribe('useDelegation', () =\u003e {\n  it('delegates events correctly', () =\u003e {\n    // Create a test controller\n    const controller = {\n      element: document.createElement('div'),\n      disconnect: () =\u003e {}\n    } as unknown as Controller\n    \n    // Add a button to test with\n    const button = document.createElement('button')\n    button.className = 'btn'\n    controller.element.appendChild(button)\n    \n    // Apply delegation\n    useDelegation(controller)\n    \n    // Set up delegation and handler\n    const handler = vi.fn()\n    controller.delegate('click', '.btn', handler)\n    \n    // Trigger event\n    button.click()\n    \n    // Verify handler was called with correct arguments\n    expect(handler).toHaveBeenCalledWith(\n      expect.any(MouseEvent),\n      button\n    )\n  })\n})\n```\n\n### E2E Tests\n\n```typescript\nimport {test, expect} from '@playwright/test';\n\ntest('delegation works with dynamic content', async ({page}) =\u003e {\n  await page.goto('/delegation-test');\n\n  // Add dynamic button\n  await page.click('#add-button');\n\n  // Click dynamic button\n  await page.click('.dynamic-btn');\n\n  await expect(page.locator('#log')).toContainText('Dynamic button clicked');\n});\n```\n\n## Performance Considerations\n\n- **Event Bubbling**: Uses native event bubbling for efficiency\n- **Single Listener**: One listener per event type, regardless of selector count\n- **Memory Management**: Automatic cleanup prevents memory leaks\n- **Selector Optimization**: Use specific selectors for better performance\n\n## Troubleshooting\n\n### Events Not Firing\n\n1.  **Check selector specificity**: Ensure your CSS selector matches the intended elements\n2.  **Verify event bubbling**: Some events don't bubble (e.g., `focus`, `blur`)\n3.  **Element containment**: Events only fire for elements within the controller's scope\n\n## License\n\nReleased under the [MIT License](LICENSE) by [Simon André](https://github.com/smnandre).\n\n---\n\n**[GitHub Repository](https://github.com/smnandre/stimulus-delegation) · [Report Issues](https://github.com/smnandre/stimulus-delegation/issues)**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmnandre%2Fstimulus-delegation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmnandre%2Fstimulus-delegation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmnandre%2Fstimulus-delegation/lists"}