Extending dd<el> with Third-Party Functionalities

dd<el> is designed with extensibility in mind. This page covers how to separate third-party functionalities and integrate them seamlessly with the library, focusing on proper resource cleanup and interoperability.

# DOM Element Extensions with Addons

The primary method for extending DOM elements in dd<el> is through the Addon pattern. Addons are functions that take an element and applying some functionality to it. This pattern enables a clean, functional approach to element enhancement.

What are Addons?

Addons are simply functions with the signature: (element) => void. They:

  • Accept a DOM element as input
  • Apply some behavior, property, or attribute to the element
// Basic structure of an addon function myAddon(config) { return function(element) { // Apply functionality to element element.dataset.myAddon = config.option; }; } // Using an addon el("div", { id: "example" }, myAddon({ option: "value" }));

# Resource Cleanup with Abort Signals

When extending elements with functionality that uses resources like event listeners, timers, or external subscriptions, it’s critical to clean up these resources when the element is removed from the DOM. dd<el> provides utilities for this through AbortSignal integration.

The scope.signal property creates an AbortSignal that automatically triggers when an element is disconnected from the DOM, making cleanup much easier to manage.

// Third-party library addon with proper cleanup function externalLibraryAddon(config, signal) { return function(element) { // Initialize the third-party library const instance = new ExternalLibrary(element, config); // Set up cleanup when the element is removed signal.addEventListener('abort', () => { instance.destroy(); }); return element; }; } // dde component function Component(){ const { signal }= scope; return el("div", null, externalLibraryAddon({ option: "value" }, signal)); }

# Building Library-Independent Extensions

When creating extensions, it’s a good practice to make them as library-independent as possible. This approach enables better interoperability and future-proofing.

✅ Library-Independent
function enhancementElement({ signal, ...config }) { // do something return function(element) { // do something signal.addEventListener('abort', () => { // do cleanup }); }; }
⚠️ Library-Dependent
// Tightly coupled to dd<el> function enhancementElement(config) { return function(element) { // do something on.disconnected(()=> { // do cleanup })(element); }; }

# Signal Extensions and Factory Patterns

Unlike DOM elements, signal functionality in dd<el> currently lacks a standardized way to create library-independent extensions. This is because signals are implemented differently across libraries.

In the future, JavaScript may include built-in signals through the TC39 Signals Proposal. dd<el> is designed with future compatibility in mind and will hopefully support these native signals without breaking changes when they become available.

The Signal Factory Pattern

A powerful approach for extending signal functionality is the "Signal Factory" pattern. This approach encapsulates specific behavior in a function that creates and configures a signal.

/** * Creates a signal for managing route state * * @param {typeof S} signal - The signal constructor */ function routerSignal(signal){ const initial = location.hash.replace("#", "") || "all"; return signal(initial, { /** * Set the current route * @param {"all"|"active"|"completed"} hash - The route to set */ set(hash){ location.hash = hash; this.value = hash; } }); } // Usage const pageS = routerSignal(S); // Update URL hash and signal value in one operation S.action(pageS, "set", "active"); // React to signal changes in the UI el("nav").append( el("a", { href: "#", className: S(()=> pageS.get() === "all" ? "selected" : ""), textContent: "All" }) );

Benefits of Signal Factories

  • Encapsulate related behavior in a single, reusable function
  • Create domain-specific signals with custom actions
  • Improve maintainability by centralizing similar logic
  • Enable better testability by accepting the signal constructor as a parameter
  • Create a clear semantic boundary around related state operations

Note how the factory accepts the signal constructor as a parameter, making it easier to test and potentially migrate to different signal implementations in the future.

Other Signal Extension Approaches

For simpler cases, you can also extend signals with clear interfaces and isolation to make future migration easier.

// Signal extension with clear interface function createEnhancedSignal(initialValue) { const signal = S(initialValue); // Extension functionality const increment = () => signal.set(signal.get() + 1); const decrement = () => signal.set(signal.get() - 1); // Return the original signal with added methods return { signal, increment, decrement }; } // Usage const counter = createEnhancedSignal(0); el("button", { textContent: "Increment", onclick: () => counter.increment() }); el("div", S.text`Count: ${counter}`);

When designing signal extensions, consider creating specialized signals for common patterns like: forms, API requests, persistence, animations, or routing. These can significantly reduce boilerplate code in your applications.

# Using Signals Independently

While signals are tightly integrated with DDE’s DOM elements, you can also use them independently. This can be useful when you need reactivity in non-UI code or want to integrate with other libraries.

There are two ways to import signals:

  1. Standard import: import { S } from "deka-dom-el/signals"; — This automatically registers signals with DDE’s DOM reactivity system
  2. Independent import: import { S } from "deka-dom-el/src/signals-lib"; — This gives you just the signal system without DOM integration
// Independent signals without DOM integration import { signal, isSignal } from "deka-dom-el/src/signals-lib"; // Create and use signals as usual const count = signal(0); const doubled = signal(() => count.get() * 2); // Subscribe to changes signal.on(count, value => console.log(value)); // Update signal value count.set(5); // Logs: 5 console.log(doubled.get()); // 10

The independent signals API includes all core functionality (S(), S.on(), S.action()).

When to Use Independent Signals

  • For non-UI state management in your application
  • When integrating with other libraries or frameworks
  • To minimize bundle size when you don't need DOM integration

# Best Practices for Extensions

  1. Use AbortSignals for cleanup: Always implement proper resource cleanup with scope.signal or similar mechanisms
  2. Separate core logic from library adaptation: Make your core functionality work with standard DOM APIs when possible
  3. Use signal factories for common patterns: Create reusable signal factories that encapsulate domain-specific behavior and state logic
  4. Document clearly: Provide clear documentation on how your extension works and what resources it uses
  5. Follow the Addon pattern: Keep to the (element) => element signature for DOM element extensions
  6. Avoid modifying global state: Extensions should be self-contained and not affect other parts of the application

Common Extension Pitfalls

Leaking event listeners or resources
Always use AbortSignal-based cleanup to automatically remove listeners when elements are disconnected
Tight coupling with library internals
Focus on standard DOM APIs and clean interfaces rather than depending on dd<el> implementation details
Mutating element prototypes
Prefer compositional approaches with addons over modifying element prototypes
Duplicating similar signal logic across components
Use signal factories to encapsulate and reuse related signal behavior
Complex initialization in addons
Split complex logic into a separate initialization function that the addon can call