Skip to content

Integrate BootstrapToggle with component-lifecycle Library #317

@palcarazm

Description

@palcarazm

Short Description of the Feature

Refactor Toggle class (BootstrapToggle) to extend Component<"toggle"> from the component-lifecycle library, delegating lifecycle management (initialization, attachment, disposal, destruction) to the library's state machine. This replaces manual flags (eventsBound, suppressExternalSync) with a deterministic, observable lifecycle while preserving the existing public API surface and domain event system.

Current behavior: Toggle manages lifecycle via manual eventsBound flag and explicit destroy() method. Construction immediately initializes and attaches the component, with all setup in constructor.

Target behavior: Toggle extends Component<"toggle"> where the constructor calls super(element), then internally invokes this.init() and this.attach(). Lifecycle hooks (doInit(), doAttach(), doDispose(), doDestroy()) encapsulate setup/teardown logic.

⚠️ PREREQUISITE BLOCKER: rerender() method must be refactored BEFORE this feature (see "Additional Comments" for details).

Expected Benefits

  • Standardized lifecycle: Deterministic state machine (idleinitializedattacheddisposeddestroyed) with validation
  • Observability: Automatic emission of typed lifecycle events (toggle:initialized, toggle:attached, toggle:disposed, toggle:destroyed) without manual dispatch
  • Resource management: Clear separation of concerns via hooks ensures proper cleanup and prevents memory leaks
  • Introspection: Built-in state query methods (isAttached(), isDestroyed(), etc.) replace manual flags
  • Zero regression: Existing public API unchanged; existing domain events (toggle:on, toggle:off, etc.) continue to work alongside new lifecycle events

Acceptance Criteria

  • Toggle extends Component<"toggle"> from component-lifecycle
  • Static readonly PREFIX property set to "toggle"
  • Constructor calls super(element) as first statement, preserves OptionResolver.resolve() call, then calls this.init() and this.attach() internally
  • doInit() hook contains: StateReducer creation (state initialization only - no DOM or side effects)
  • doAttach() hook contains: DOMBuilder creation, bindEventListeners(), interceptInputProperties(), and setting this.element.bsToggle = this
  • doDispose() hook removes event listeners (reverse of doAttach())
  • doDestroy() hook contains current destroy() logic: restoreInputProperties(), unbindEventListeners(), domBuilder.destroy(), and delete this.element.bsToggle
  • Manual eventsBound flag is removed; replaced with isAttached() or lifecycle state checks
  • suppressExternalSync flag remains (internal state sync mechanism, not lifecycle-related)
  • All existing public methods (toggle, on, off, enable, disable, readonly, indeterminate, determinate, update, destroy) continue to work without signature changes
  • Existing domain events (toggle:on, toggle:off, toggle:mixed, toggle:enabled, toggle:disabled, toggle:readonly) are still dispatched via trigger() in addition to new lifecycle events
  • Auto-initialization via [data-toggle="toggle"] continues to work (constructor handles init()/attach() internally)
  • Existing tests pass with no regressions

Documentation

Event Emission Flow

Lifecycle events (automatic from base class with PREFIX = "toggle"):

  • toggle:initialized → emitted after doInit() completes
  • toggle:attached → emitted after doAttach() completes
  • toggle:disposed → emitted after doDispose() completes
  • toggle:destroyed → emitted after doDestroy() completes

Domain events (preserved existing behavior from trigger() method):

  • toggle:on → emitted when toggle becomes ON
  • toggle:off → emitted when toggle becomes OFF
  • toggle:mixed → emitted when toggle becomes MIXED (indeterminate)
  • toggle:enabled → emitted when toggle becomes enabled
  • toggle:disabled → emitted when toggle becomes disabled
  • toggle:readonly → emitted when toggle becomes readonly

Backward compatibility: Existing listeners for domain events continue to work unchanged. Lifecycle events are additive.

See component-lifecycle events doc

State Query Methods (New Capabilities)

These methods from Component become available without additional implementation:

Method Replaces
isAttached() eventsBound flag (indirectly)
isDestroyed() Manual tracking
isDisposed() New capability
isInitialized() New capability
isIdle() New capability
state (getter) Returns current LifecycleState enum

See component-lifecycle lifecycle doc

Public API Compatibility Matrix

Method Current behavior New behavior Regression risk
toggle(silent?) Changes state, renders, triggers events Same (delegates to stateReducer.do()) None
on(silent?) Same as above Same None
off(silent?) Same as above Same None
enable(silent?) Same as above Same None
disable(silent?) Same as above Same None
readonly(silent?) Same as above Same None
indeterminate(silent?) Same as above Same None
determinate(silent?) Same as above Same None
update() Syncs state from element, re-renders Same None
destroy() Cleans up, deletes reference Delegate in Component implementation None (idempotent)
rerender() TO BE REFACTORED (prerequisite) TBD in separate PR N/A - blocker

component-lifecycle API

See component-lifecycle API doc

BLOCKER: rerender() Method Refactoring

Current implementation (lines 477-480):

rerender() {
    this.destroy();
    const _ = new Toggle(this.element, this.userOptions);
}

Problem with lifecycle integration:

  • Destroys current instance, creates entirely new instance
  • New instance has no relationship to original (different object reference)
  • Lifecycle events emitted from different instances would confuse listeners
  • Circumvents the state machine entirely

Required refactoring (to be done BEFORE this feature):

Additional Comments

Risks & Mitigations

Risk Mitigation
BLOCKER - rerender() incompatibility Separate PR required before this feature; issue will be opened and tracked independently
Property interception in doAttach() Ensures element is in DOM before intercepting properties; no functional regression
Domain event naming overlap Lifecycle events use toggle:initialized, domain events use toggle:on - no collision despite same prefix
suppressExternalSync flag Remains in doAttach(); lifecycle migration doesn't affect this internal sync mechanism
DOMBuilder not observing lifecycle DOMBuilder is a helper class, not a component; it remains unchanged and is destroyed via domBuilder.destroy() in doDestroy()
Test suite updates Existing mocks may assume eventsBound flag exists; update tests to use isAttached() or remove flag assertions

Dependencies

  • Required package: component-lifecycle (version as specified in user's links)
  • No peer dependency changes: Bootstrap 5, existing CSS remain unchanged

Out of Scope

  • Changing the public API surface (methods, parameters, return types)
  • Modifying existing domain events (toggle:on, etc.) behavior or payload
  • Refactoring rerender() (handled in separate prerequisite PR)
  • Changing DOMBuilder, OptionResolver, or StateReducer beyond lifecycle integration
  • Migrating existing instances or providing codemods

Feature Request Checklist

  • Confirm that you agree to follow the project's code of conduct.
  • Confirm that you have reviewed open and rejected feature requests to ensure novelty.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status

    Needs triage

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions