npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dooboostore/simple-web-component

v1.0.45

Published

Simple Web Component library based on dom-render philosophy

Downloads

1,221

Readme

@dooboostore/simple-web-component (SWC)

NPM version Build and Test License: MIT

SWC is a lightweight, production-ready Web Components framework for building fast, modular, and maintainable SPAs without Virtual DOM overhead.


🎯 Core Features

1. @elementDefine - Component Registration

Register Web Components with automatic lifecycle management and DI support.

@elementDefine('my-component')
class MyComponent extends HTMLElement {
  @onInitialize
  onconstructor(service: MyService) {
  }
}

2. Dependency Injection (@onInitialize)

Inject services into Web Components using the @onInitialize decorator.

@elementDefine('dashboard')
class Dashboard extends HTMLElement {
  @onInitialize
  onconstructor(
    @inject(UserService.SYMBOL) userService: UserService
  ) {
    this.userService = userService;
  }
}

3. Declarative DOM Updates

@onConnectedShadow

Render HTML automatically when component connects to DOM.

@onConnectedShadow
render() {
  return `<div>Hello, <span>${this.name}</span>!</div>`;
}

@innerHtmlLight

Apply HTML to specific host element.

@innerHtmlLight
updateContent() {
  return `<p>Updated content</p>`;
}

@replaceChildrenLight

Replace child nodes with new content.

@replaceChildrenLight
replaceChildren(node: Node) {
  return node;
}

3.5 Slot Management with @applySlot

Manage dynamic content insertion into named slots using slot decorators. Slots are placeholders in templates that can be updated dynamically.

@elementDefine('content-manager')
class ContentManager extends HTMLElement {
  // Method: Replace children in slot with HTML
  @addEventListener('.update-btn', 'click')
  @replaceChildrenHtmlSlot('main-content')
  updateContent() {
    return '<div>Updated content here</div>';
  }

  // Method: Append HTML to slot
  @appendHtmlSlot('main-content')
  addMoreContent() {
    return '<p>Additional content</p>';
  }

  // Method: Append text to slot
  @appendTextSlot('sidebar')
  addSidebarText() {
    return 'Sidebar text: ' + new Date().toISOString();
  }

  // Method: Clear slot content
  @addEventListener('.clear-btn', 'click')
  @clearSlot('main-content')
  clearContent() {
    return true;
  }

  @onConnectedLight
  render() {
    return `
      <div>
        <h2>Main Content</h2>
        <!--[[ main-content ]]-->
        
        <aside>
          <h3>Sidebar</h3>
          <!--[[ sidebar ]]-->
        </aside>
        
        <button class="update-btn">Update Content</button>
        <button class="clear-btn">Clear Content</button>
      </div>
    `;
  }
}

Slot Syntax in Templates:

  • <!--[[ slot-id ]]--> - Named slot placeholder (HTML comment syntax)

@applySlot Decorator Variants:

  • clearSlot(slotId) - Clear all content
  • prependHtmlSlot(slotId) - Add HTML at beginning
  • prependTextSlot(slotId) - Add text at beginning
  • appendHtmlSlot(slotId) - Add HTML at end
  • appendTextSlot(slotId) - Add text at end
  • replaceChildrenHtmlSlot(slotId) - Replace all with HTML
  • replaceChildrenTextSlot(slotId) - Replace all with text

Position Options:

export type ApplySlotPosition = 
  | 'prepend'
  | 'prependHtml'        // Add HTML at beginning
  | 'prependText'        // Add text at beginning
  | 'append'         
  | 'appendHtml'         // Add HTML at end
  | 'appendText'         // Add text at end
  | 'replaceChildren'
  | 'replaceChildrenHtml' // Replace all with HTML
  | 'replaceChildrenText' // Replace all with text
  | 'clear';             // Clear all content

3.6 State Management with @state

Reactive state management with automatic DOM updates when state changes. State values are accessible in templates and scripts using special syntax.

@elementDefine('counter-app')
class CounterApp extends HTMLElement {
  // Declare reactive state
  @state('count')
  count: number = 0;

  @state('message')
  message: string = 'Hello';

  @state('isActive')
  isActive: boolean = false;

  @addEventListener('button', 'click')
  increment() {
    this.count++;  // Triggers automatic DOM update
  }

  @onConnectedInnerHtml
  render() {
    return `
      <div>
        <!-- HTML directive: render HTML content -->
        <!--[html @message@ ]-->
        
        <!-- Text directive: render as text -->
        <!--[text Count: @count@ ]-->
        
        <!-- Attribute binding with a: prefix -->
        <div a:title="'Count is'+@count@"></div>
        
        <!-- Event binding with e: prefix -->
        <button e:click="@increment@()">Increment (@count@)</button>
        
        <!-- Conditional rendering -->
        <div a:style="@isActive@ ? 'color: green' : 'color: red'">
        </div>
      </div>
    `;
  }
}

State Syntax in Templates:

Reading State Values(read only):

  • @stateName@ - Read state value as text/attribute
  • @stateName@() - Call state property as function (if it's a function)
  • @expression@ - Evaluate expressions with state variables

Template Directives:

  • <!--[html @state@ ]--> - Render state value as HTML
  • <!--[text @state@ ]--> - Render state value as text
  • a:attributeName="@state@" - Bind state to HTML attributes
  • e:eventName="@methodName@()" - Bind events to methods

How @state Works:

  1. Declare property with @state('stateName')
  2. When property value changes via this.count++, setter is triggered
  3. Setter automatically applies state context to templates
  4. All @stateName@ expressions in templates are re-evaluated
  5. DOM updates automatically with new values

State Context Available:

  • @propertyName@ - Access state properties
  • @methodName@() - Call methods
  • @expression@ - Evaluate JavaScript expressions
  • All state variables available in template expressions

Important: State Properties are Read-Only in Templates State values accessed in templates via @stateName@ syntax are read-only. You cannot assign values to state properties from within template expressions. State must be updated from component methods:

// ✅ CORRECT - Update state from method
@addEventListener('button', 'click')
increment() {
  this.count++;  // Direct property assignment in code
}

// ❌ WRONG - Cannot assign in template
<!--[text @count = 5@ ]-->  // This won't work - read-only in templates

// ✅ CORRECT - Call method from template
<button e:click="@increment@()">Increment</button>

Advanced Example:

@elementDefine('user-profile')
class UserProfile extends HTMLElement {
  @state('user')
  user = { name: 'Alice', age: 30, email: '[email protected]' };

  @state('isEditing')
  isEditing = false;

  @addEventListener('.edit-btn', 'click')
  toggleEdit() {
    this.isEditing = !this.isEditing;
  }

  @onConnectedShadow
  render() {
    return `
      <div>
        <!-- Display user info -->
        <h2><!--[text @user.name@ ]--></h2>
        <p a:title="Email: @user.email@">Age: @user.age@</p>
        
        <!-- Conditional rendering based on state -->
        <div a:style="@isEditing@ ? 'border: 1px solid blue' : 'border: none'">
          <!--[html @isEditing@ ? '<input type="text" />' : '<span>View Mode</span>' ]-->
        </div>
        
        <button e:click="toggleEdit"> toggleEdit </button>
      </div>
    `;
  }
}

4. Event Handling

@addEventListener

Attach event listeners to elements with optional filter support.

@addEventListener('#submit-btn', 'click')
onSubmit(event: Event) {
  console.log('Submitted');
}

// Filter events - only process matching events
@addEventListener('button', 'click', {
  filter: (event, helper) => {
    return event.target?.id === 'critical-button';
  }
})
onCriticalClick(event: Event) {
  console.log('Critical action');
}

@addEventListener Decorator Variants:

  • addEventListener(selector, type, options) - Full form
  • event(selector, type, options) - Short alias
  • addEventListenerThis(type, options) - Listen on $this element
  • addEventListenerAppHost(type, options) - Listen on $appHost
  • addEventListenerWindow(type, options) - Listen on window
  • addEventListenerDocument(type, options) - Listen on document
  • addEventListenerDelegate(selector, type, options) - Event delegation
  • eventDelegate(selector, type, options) - Short alias
  • addEventListenerDelegateLightDom(selector, type, options) - Delegate in light DOM
  • eventDelegateLightDom(selector, type, options) - Short alias
  • addEventListenerDelegateShadowDom(selector, type, options) - Delegate in shadow DOM
  • eventDelegateShadowDom(selector, type, options) - Short alias
  • addEventListenerDelegateAllDom(selector, type, options) - Delegate in all DOM
  • eventDelegateAllDom(selector, type, options) - Short alias

@emitCustomEvent

Emit custom events with data.

@emitCustomEvent('$appHost', 'user-login')
async onLogin() {
  const user = await this.authService.login();
  return { user };  // Sent as event detail
}

@emitCustomEvent Decorator Variants:

  • emitCustomEvent(target, type, options) - Full form
  • emit(target, type, options) - Short alias
  • emitCustomEventThis(type, options) - Emit from $this element

@publishSwcAppMessage

Publish messages through the message bus when method completes.

@publishSwcAppMessage()
async onLogin() {
  const user = await this.authService.login();
  return user;  // Published as message with data: user
}

@publishSwcAppMessage('user-profile-updated')
updateProfile() {
  const profile = { name: this.name, email: this.email };
  return profile;  // Published as message with type: 'user-profile-updated'
}

@publishSwcAppMessage Decorator Variants:

  • publishSwcAppMessage() - Publish without message type
  • publishSwcAppMessage(messageType) - Publish with specific message type
  • publishSwcAppMessage(messageType, { valueKey: 'customKey' }) - Publish with custom value extraction
  • publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' }) - Publish with options object
  • publish() - Short alias
  • publish(messageType) - Short alias with message type
  • publish(messageType, options) - Short alias with options

Using valueKey for Multiple Decorators:

@publishSwcAppMessage('event1', { valueKey: 'detail1' })
@publishSwcAppMessage('event2', { valueKey: 'detail2' })
handleMultipleEvents() {
  return {
    detail1: { type: 'event1', data: 'value1' },
    detail2: { type: 'event2', data: 'value2' }
  };
}

@emitCustomEventThis

Emit events from a method with custom event name mapping.

@emitCustomEventThis('navigate', { attributeName: 'on-navigate' })
onNavClick(e: any) {
  return { path: e.target.dataset.path };
}

@addEventListenerThis

Listen to events on the component element itself ($this selector).

@addEventListenerThis('click')
onHostClick(event: Event) {
  console.log('Host element clicked');
}

@addEventListenerAppHost

Listen to events on the app root host element ($appHost selector). Enables selective event handling with filters.

// Listen to all user-action events from $appHost
@addEventListenerAppHost('user-action')
onUserAction(e: CustomEvent) {
  console.log('User action:', e.detail);
}

// Filter specific events - loose coupling pattern
@addEventListenerAppHost('user-action', {
  filter: (event, helper) => event.detail?.type === 'login'
})
onUserLogin(e: CustomEvent) {
  console.log('User logged in:', e.detail.userName);
}

// Different component filtering same event differently
@addEventListenerAppHost('user-action', {
  filter: (event, helper) => event.detail?.type === 'logout'
})
onUserLogout(e: CustomEvent) {
  console.log('User logged out');
}

5. DOM Querying

@query

Query single element (Light DOM by default).

@query('#form-input')
formInput?: HTMLInputElement;

@query('.card', { root: 'shadow' })
shadowCard?: HTMLElement;

@queryAll

Query multiple elements.

@queryAll('input[type="text"]')
textInputs?: HTMLInputElement[];

@queryAll('li', { root: 'shadow' })
listItems?: HTMLLIElement[];

@queryThis

Query the component element itself. == this

@queryThis
self?: HTMLElement;

printTag() {
  console.log(this.self?.tagName);  // CUSTOM-CARD
}

6. Attribute Binding

@attribute (Field Decorator)

Bind HTML attributes to properties with automatic read/write synchronization.

Intelligently distinguishes between attribute names and CSS selectors:

@elementDefine('product-card')
class ProductCard extends HTMLElement {
  // Attribute name on $this (auto-detected)
  @attribute('product-id')
  productId: string;

  // Attribute name on selector
  @attribute('#user', 'data-id')
  userId: string;

  // Bare decorator (uses field name as attribute)
  @attribute
  title: string;

  @onInitialize
  onconstructor() {
    if (this.productId) {
      this.loadProduct(this.productId);
    }
  }
}

@setAttribute (Method Decorator)

Set element attributes from method return values.

@elementDefine('status-updater')
class StatusUpdater extends HTMLElement {
  // Set attribute on $this
  @setAttribute('data-status')
  updateStatus() {
    return this.isActive ? 'active' : 'inactive';
  }

  // Set attribute on selector
  @setAttribute('#user', 'data-name')
  setUserName() {
    return this.userName;
  }

  // Bare decorator (uses method name as attribute)
  @setAttribute
  setValue() {
    return this.computedValue;
  }
}

Usage Patterns:

  • @attribute('attr-name') - Attribute name on $this (auto-detected)
  • @attribute('#selector', 'attr-name') - Attribute name on selector
  • @attribute('attr-name', options) - Attribute name on $this with options
  • @attribute('#selector', 'attr-name', options) - Attribute name on selector with options
  • @attribute - Bare decorator (uses field name as attribute on $this)

@setAttribute Patterns:

  • @setAttribute('attr-name') - Set attribute on $this
  • @setAttribute('#selector', 'attr-name') - Set attribute on selector
  • @setAttribute('attr-name', options) - Set attribute on $this with options
  • @setAttribute('#selector', 'attr-name', options) - Set attribute on selector with options
  • @setAttribute - Bare decorator (uses method name as attribute on $this)

@changedAttribute (Method Decorator)

Listen for attribute changes on the component this element.

@elementDefine('reactive-component')
class ReactiveComponent extends HTMLElement {
  @changedAttribute('product-id')
  onProductIdChanged(newValue: string, oldValue: string) {
    console.log(`Product changed from ${oldValue} to ${newValue}`);
    this.loadProduct(newValue);
  }

  @changedAttribute('data-status', { type: Boolean })
  onStatusChanged(newValue: boolean) {
    console.log(`Status is now: ${newValue}`);
  }

  // Listen to any attribute change
  @changedAttribute()
  onAnyAttributeChanged(newValue: any, oldValue: any) {
    console.log('An attribute changed');
  }
}

@changedAttribute Options:

  • attributeName - Attribute name to listen for (optional, defaults to method name)
  • type - Type converter: Number, Boolean, or String
  • while - Execution condition: 'connected' (only while connected to DOM)

6.5 Property Binding

@property (Field Decorator)

Bind element properties to component fields with automatic read/write synchronization.

@elementDefine('input-wrapper')
class InputWrapper extends HTMLElement {
  // Property on $this (auto-detected)
  @property('value')
  inputValue: string;

  // Property on selector
  @property('#submit-btn', 'disabled')
  isSubmitDisabled: boolean;

  // Bare decorator (uses field name as property)
  @property
  checked: boolean;
}

@setProperty (Method Decorator)

Set element properties from method return values.

@elementDefine('form-handler')
class FormHandler extends HTMLElement {
  // Set property on $this
  @setProperty('disabled')
  disableForm() {
    return !this.isValid();
  }

  // Set property on selector
  @setProperty('#submit-btn', 'disabled')
  updateSubmitState() {
    return this.hasErrors;
  }

  // Bare decorator (uses method name as property)
  @setProperty
  setValue() {
    return this.computedValue;
  }
}

Usage Patterns:

  • @property('propertyName') - Property on $this
  • @property('#selector', 'propertyName') - Property on selector
  • @property('propertyName', options) - Property on $this with options
  • @property('#selector', 'propertyName', options) - Property on selector with options
  • @property - Bare decorator (uses field name as property on $this)

@setProperty Patterns:

  • @setProperty('propertyName') - Set property on $this
  • @setProperty('#selector', 'propertyName') - Set property on selector
  • @setProperty('propertyName', options) - Set property on $this with options
  • @setProperty('#selector', 'propertyName', options) - Set property on selector with options
  • @setProperty - Bare decorator (uses method name as property on $this)

7. DOM Manipulation with applyNode

Surgically add, replace, or remove nodes in the DOM with fine-grained control.

@elementDefine('content-updater')
class ContentUpdater extends HTMLElement {
  @addEventListener('button', 'click')
  @replaceChildren()
  updateContent() {
    return `<div>New content</div>`;
  }

  @addEventListener('.append-btn', 'click')
  @beforeEndNode()
  appendContent() {
    return `<p>Appended content</p>`;
  }

  @addEventListener('.prepend-btn', 'click')
  @afterBegin()
  prependContent() {
    return `<p>Prepended content</p>`;
  }

  @onConnectedLight
  render() {
    return `
      <div>
        <button class="append-btn">Append</button>
        <button class="prepend-btn">Prepend</button>
        <button>Replace</button>
      </div>
    `;
  }
}

@applyNode Decorator Variants:

  • replaceChildren() - Replace all children
  • replaceChildrenLight() - Replace children in light DOM
  • beforeEnd() - Append to end
  • beforeEndLight() - Append to light DOM end
  • afterBegin() - Prepend to beginning
  • afterBeginLight() - Prepend to light DOM beginning
  • innerHtml() - Set innerHTML
  • innerHtmlLight() - Set innerHTML in light DOM
  • innerText() - Set innerText

Position Options:

export type ApplyNodePosition = 
  | 'beforeBegin'      // Before element
  | 'afterBegin'       // After element opens
  | 'beforeEnd'        // Before element closes
  | 'afterEnd'         // After element closes
  | 'replace'          // Replace element
  | 'replaceChildren'  // Replace children
  | 'innerHtml'        // Set innerHTML
  | 'innerText'        // Set innerText
  | 'remove';          // Remove element

8. Style & Class Management

Apply styles and classes dynamically with @applyStyle and @applyClass.

@elementDefine('styled-component')
class StyledComponent extends HTMLElement {
  @state('isActive')
  isActive = false;

  @addEventListener('button', 'click')
  @updateStyle()
  toggleStyle() {
    return {
      color: this.isActive ? 'green' : 'red',
      fontSize: '16px'
    };
  }

  @addEventListener('.toggle-btn', 'click')
  @updateClass()
  toggleClass() {
    return {
      'active': this.isActive,
      'disabled': !this.isActive
    };
  }

  @onConnectedLight
  render() {
    return `
      <div>
        <button class="toggle-btn">Toggle</button>
        <button>Update Style</button>
      </div>
    `;
  }
}

@applyStyle Variants:

  • setStyleThis() - Clear and set styles
  • updateStyleThis() - Update/merge styles
  • removeStyleThis() - Remove specific styles

@applyClass Variants:

  • setClassThis() - Replace all classes
  • updateClassThis() - Toggle classes
  • addClassThis() - Add classes
  • removeClassThis() - Remove classes
  • toggleClassThis() - Toggle classes

8.5 Lifecycle Hooks

Lifecycle hooks allow you to execute code at specific points in a component's lifecycle. All lifecycle decorators support optional order parameter for execution ordering.

@elementDefine('my-component')
class MyComponent extends HTMLElement {
  @onInitialize({ order: 0 })
  onInit() {
    // Called during component construction
    console.log('Component initialized');
  }

  @onConnectedBefore({ order: 0 })
  beforeConnected() {
    // Called before element connects to DOM
  }

  @onConnectedAfter({ order: 1 })
  afterConnected() {
    // Called after element connects to DOM
  }

  @onConnected
  onConnected() {
    // Called when element enters DOM
  }

  @onDisconnectedBefore
  beforeDisconnected() {
    // Called before element disconnects from DOM
  }

  @onDisconnected
  onDisconnected() {
    // Called when element leaves DOM
  }

  @onAdoptedBefore
  beforeAdopted() {
    // Called before element is adopted into new document
  }

  @onAdopted
  onAdopted() {
    // Called when element is adopted into new document
  }

  @onConnectedSwcApp({ order: 0 })
  onSwcAppConnected() {
    // Called after SwcApp.connect() completes
    // Full DI support available through hostSet
  }

  @onConnectedCompleted({ order: 0 })
  onConnectedCompleted() {
    // Called after all connected lifecycle hooks complete
  }
}

Lifecycle Execution Order:

  • @onInitialize - Component construction
  • @onConnectedBefore - Before DOM connection
  • @onConnectedAfter - After DOM connection
  • @onConnected - DOM connection (with HTML rendering)
  • @onConnectedSwcApp - After SwcApp initialization
  • @onConnectedCompleted - All connected hooks done
  • @onDisconnectedBefore - Before DOM disconnection
  • @onDisconnected - DOM disconnection
  • @onAdoptedBefore - Before document adoption
  • @onAdopted - Document adoption

Order Parameter: All lifecycle decorators support order?: number for controlling execution sequence:

@onConnectedBefore({ order: 0 })  // Runs first
onFirst() { }

@onConnectedBefore({ order: 1 })  // Runs second
onSecond() { }

9. Structural Directives

Structural Directives allow declarative conditional rendering, list iteration, async handling, and routing with automatic attribute substitution and dynamic expressions.

8.0 Expression Syntax - Dynamic Evaluation

SWC supports two types of expression syntax for dynamic value evaluation:

`{{ }} - Standard Expression Evaluation

  • Used for boolean conditions and value comparisons
  • Syntax: {{ expression }}
  • Example: {{ $value === 'pending' }}, {{ $host.isLoggedIn }}
  • Context variables: $value, $item, $index, $host, $parentHost, $appHost, etc.

`{{= }} - Function Call & Return Expression

  • Evaluates JavaScript expressions and returns result
  • Syntax: {{= functionCall() }} or {{= computedValue }}
  • Automatically executes functions and captures return values
  • Example: {{= $item.name }}, {{= $parentHost.getData() }}, {{= formatDate($item.date) }}
  • Used in attributes for dynamic substitution
  • Result is converted to string for DOM attributes

Context Variables Available in All Directives:

  • $value - the value passed to the template
  • $host - the current Web Component instance
  • $parentHost - parent component
  • $appHost - root application component
  • $item - current item (in SwcLoop)
  • $index - current index (in SwcLoop)
  • Helper functions - utility methods from SwcUtils.getHelperAndHostSet()

9. Component Communication - Message Bus

SWC provides a powerful message bus system for inter-component communication through SwcApp. Components can publish and subscribe to typed messages while connected.

@subscribeSwcAppMessageWhileConnected

Subscribe to messages while component is connected to DOM.

@elementDefine('notification-panel')
class NotificationPanel extends HTMLElement {
  @subscribeSwcAppMessageWhileConnected
  onAnyMessage(message: SwcAppMessage) {
    console.log('Received message:', message);
  }

  @subscribeSwcAppMessageWhileConnected('user-login')
  onUserLogin(message: SwcAppMessage<{ username: string }>) {
    console.log(`Welcome ${message.data?.username}`);
  }

  @subscribeSwcAppMessageWhileConnected('user-login', {
    filter: (msg) => msg.data?.username === 'admin'
  })
  onAdminLogin(message: SwcAppMessage) {
    console.log('Admin logged in!');
  }
}

@publishSwcAppMessage

Publish a message from a method's return value.

@elementDefine('login-form')
class LoginForm extends HTMLElement {
  @publishSwcAppMessage()
  async onSubmit() {
    const user = await this.authService.login(
      this.username,
      this.password
    );
    return user;  // Published as message with data: user
  }

  @publishSwcAppMessage('user-profile-updated')
  updateProfile() {
    const profile = { name: this.name, email: this.email };
    return profile;  // Published as message with type: 'user-profile-updated'
  }
}

@publishSwcAppMessage Decorator Variants:

  • publishSwcAppMessage() - Publish without message type
  • publishSwcAppMessage(messageType) - Publish with specific message type
  • publishSwcAppMessage(messageType, { valueKey: 'customKey' }) - Publish with custom value extraction
  • publishSwcAppMessage({ messageType: 'type', valueKey: 'customKey' }) - Publish with options object
  • publishMessage() - Short alias
  • publishMessage(messageType) - Short alias with message type
  • publishMessage(messageType, options) - Short alias with options

Using valueKey for Multiple Decorators:

@publishSwcAppMessage('event1', { valueKey: 'detail1' })
@publishSwcAppMessage('event2', { valueKey: 'detail2' })
handleMultipleEvents() {
  return {
    detail1: { type: 'event1', data: 'value1' },
    detail2: { type: 'event2', data: 'value2' }
  };
}

SwcAppMessage Structure:

type SwcAppMessage<T = any> = {
  publisher?: any;        // Component that published the message
  data?: T;              // Payload data
  type?: string;         // Optional message type/category
};

Key Features:

  • Lifecycle-aware - subscriptions only active while component is connected
  • Type-safe - specify message types for filtering
  • Filter support - use custom filter functions to handle specific conditions
  • Auto-publishing - return value automatically becomes message payload
  • Async support - works with async methods (promises)
  • Centralized - all messages routed through SwcApp host
  • Decoupled - components don't need to know about each other

Usage Example:

// Publisher component
@elementDefine('product-list')
class ProductList extends HTMLElement {
  @publishSwcAppMessage('product-selected')
  selectProduct(productId: string) {
    const product = this.findProduct(productId);
    return product;
  }
}

// Subscriber component
@elementDefine('product-detail')
class ProductDetail extends HTMLElement {
  @subscribeSwcAppMessageWhileConnected('product-selected')
  onProductSelected(message: SwcAppMessage<Product>) {
    this.displayProduct(message.data);
  }
}

11. SwcApp - Multiple Element Types with Mixin Pattern

SWC provides a flexible Mixin-based architecture for creating SwcApp elements that extend different HTMLElement types. This enables building SPAs with any semantic HTML element as the root.

Available SwcApp Variants

import {
  SwcApp,           // swc-app (HTMLElement)
  SwcAppBody,       // swc-app-body (HTMLBodyElement, is="body")
  SwcAppDiv,        // swc-app-div (HTMLDivElement, is="div")
  SwcAppSection,    // swc-app-section (HTMLElement, is="section")
  SwcAppMain,       // swc-app-main (HTMLElement, is="main")
  SwcAppArticle,    // swc-app-article (HTMLElement, is="article")
  SwcAppHeader,     // swc-app-header (HTMLElement, is="header")
  SwcAppFooter,     // swc-app-footer (HTMLElement, is="footer")
  SwcAppNav,        // swc-app-nav (HTMLElement, is="nav")
  SwcAppAside,      // swc-app-aside (HTMLElement, is="aside")
  defineSwcAppAll,  // Register all SwcApp variants at once
  swcAppFactories   // Array of all definition functions
} from '@dooboostore/simple-web-component';

Using SwcAppBody (Recommended for SPAs)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>My SPA</title>
</head>
<!-- Use is="swc-app-body" for semantic HTML -->
<body id="app" is="swc-app-body">
  <root-router></root-router>
</body>
<script src="bundle.js"></script>
</html>
// index.ts
import 'reflect-metadata';
import { defineSwcAppBody, SwcAppInterface, defineSwcAppAll } from '@dooboostore/simple-web-component';
import bootFactory from './bootFactory';

const w = window;

w.document.addEventListener('DOMContentLoaded', async () => {
  const container = Symbol('app');
  
  // Initialize services and components
  await defineSwcAppBody(w);


  // Get app root element
  const appElement = w.document.querySelector('#app') as SwcAppInterface;
  
  if (appElement && typeof appElement.connect === 'function') {
    appElement.connect({
      path: '/',
      routeType: 'path',
      container: container,
      onStartedLazyDefineComponent: [yourComponentFactory1, yourComponentFactor2],
      window: w,
      onEngineStarted: () => {
        console.log('🚀 Application started successfully');
      }
    });
  }
});

Factory Pattern - Component Registration

// Component factory
export default (w: Window) => {
  const tagName = 'my-component';
  const existing = w.customElements.get(tagName);
  if (existing) return tagName;

  @elementDefine(tagName, { window: w })
  class MyComponent extends w.HTMLElement {
    @onInitialize
    onconstructor(@inject(MyService.SYMBOL) service: MyService) {
      this.service = service;
    }
  }

  return tagName;
};

// Boot factory
export default (w: Window, container: symbol) => {
  serviceFactories.forEach(s => s(container));
};

// Entry point
const container = Symbol('app');
await defineSwcAppBody(w);
const appElement = w.document.querySelector('#app') as SwcAppInterface;
appElement.connect({
  container,
  window: w,
  onStartedLazyDefineComponent: [...pageFactories, ...componentFactories],
  onEngineStarted: () => {
    appElement.innerHTML = '<root-router></root-router>';
  }
});

12. Routing with @subscribeSwcAppRouteChangeWhileConnected

SWC provides declarative routing with automatic path matching, order-based execution, and optional propagation control.

Basic Routing Setup

@elementDefine('root-router')
class RootRouter extends HTMLElement {
  @subscribeSwcAppRouteChangeWhileConnected(['', '/'], { order: 0 })
  @innerHtmlLight
  handleHome(routerPathSet: RouterEventType) {
    return `<landing-page/>`;
  }

  @subscribeSwcAppRouteChangeWhileConnected(['/products'], { order: 1 })
  @innerHtmlLight
  handleProducts(routerPathSet: RouterEventType) {
    return `<products-list/>`;
  }

  @subscribeSwcAppRouteChangeWhileConnected(['/product/{id}'], { order: 2 })
  @innerHtmlLight
  handleProductDetail(routerPathSet: RouterEventType) {
    const { id } = routerPathSet.pathData;
    return `<product-detail product-id="${id}"/>`;
  }

  @subscribeSwcAppRouteChangeWhileConnected(['/{tail:.*}'], { order: 999 })
  @innerHtmlLight
  handle404(routerPathSet: RouterEventType) {
    return `<not-found-page/>`;
  }
}

Route Handler Features

1. Path Matching

  • No path pattern (omit path): @subscribeSwcAppRouteChangeWhileConnected({ order: -1 }) - matches all routes
  • Exact match: ['', '/'] - matches home route only
  • Prefix match: ['/products'] - matches /products and /products/...
  • Dynamic segments: ['/product/{id}'] - captures id parameter
  • Wildcard: ['/{tail:.*}'] - matches any remaining path (use for 404)

2. Order-Based Execution Routes are executed in order of order value (lowest first). First matching route with a return value stops propagation:

@subscribeSwcAppRouteChangeWhileConnected(['', '/'], { order: 0 })  // Checked first
@subscribeSwcAppRouteChangeWhileConnected(['/admin'], { order: 1 })  // Checked second
@subscribeSwcAppRouteChangeWhileConnected(['/{tail:.*}'], { order: 999 })  // Checked last (404)

3. Propagation Control

  • Return a value (HTML string) → Stops propagation to next handlers
  • Return undefined/null → Continues to next handler
// This handler stops propagation (returns HTML)
@subscribeSwcAppRouteChangeWhileConnected(['/admin'], { order: 1 })
@innerHtmlLight
handleAdmin(routerPathSet: RouterEventType) {
  return `<admin-panel/>`; // ✅ Stops here
}

// This handler continues propagation (no return value)
@subscribeSwcAppRouteChangeWhileConnected({ order: -1 })
onRouteChange(routerPathSet: RouterEventType) {
  console.log('Route changed:', routerPathSet.path);
  // No return value → continues to next handler
}

Advanced Example with Logging

@elementDefine('accommodation-router')
class AccommodationRouter extends HTMLElement {
  // Global route logger (order: -1 runs first, no path pattern = matches all routes)
  @subscribeSwcAppRouteChangeWhileConnected({ order: -1 })
  onRouteChange(routerPathSet: RouterEventType) {
    console.log('[Route Change]', {
      path: routerPathSet.path,
      pathData: routerPathSet.pathData,
      timestamp: new Date().toISOString()
    });
    // No return → continues to route handlers
  }

  // Home route
  @subscribeSwcAppRouteChangeWhileConnected(['', '/'], { order: 0 })
  @innerHtmlLight
  handleHome(routerPathSet: RouterEventType) {
    console.log('[Route Handler] Home');
    return `<landing-page/>`;
  }

  // List route
  @subscribeSwcAppRouteChangeWhileConnected(['/list'], { order: 1 })
  @innerHtmlLight
  handleList(routerPathSet: RouterEventType) {
    console.log('[Route Handler] List');
    return `<list-page/>`;
  }

  // Detail route with dynamic parameter
  @subscribeSwcAppRouteChangeWhileConnected(['/detail/{productId}'], { order: 2 })
  @innerHtmlLight
  handleDetail(routerPathSet: RouterEventType) {
    const { productId } = routerPathSet.pathData;
    console.log('[Route Handler] Detail', { productId });
    return `<detail-page product-id="${productId}"/>`;
  }

  // 404 fallback (order: 999 runs last)
  @subscribeSwcAppRouteChangeWhileConnected(['/{tail:.*}'], { order: 999 })
  @innerHtmlLight
  handle404(routerPathSet: RouterEventType) {
    console.log('[Route Handler] 404 Not Found', routerPathSet.path);
    return `<not-found-page/>`;
  }
}

Router Configuration Options

interface SwcAppRouteChangeOptions {
  path?: RoutePathType;  // Route path pattern(s)
  order?: number;        // Execution order (default: 0)
  filter?: (router: Router, meta: {currentThis: any, helper: HelperHostSet}) => boolean;
}

Usage:

@subscribeSwcAppRouteChangeWhileConnected(
  ['/admin/{section}'],
  {
    order: 5,
    filter: (router, meta) => meta.currentThis.isAdmin === true
  }
)
@innerHtmlLight
handleAdminSection(routerPathSet: RouterEventType) {
  const { section } = routerPathSet.pathData;
  return `<admin-${section}/>`;
}

RouterEventType Structure

type RouterEventType = {
  path: string;           // Current route path
  pathData: Record<string, any>;  // Extracted path parameters
  triggerPoint: 'start' | 'end';  // Route change phase
};

Example with path parameters:

Route: /product/{id}
URL: /product/123
pathData: { id: '123' }

Multiple Decorators with Shared Return Value

When using multiple decorators on the same method, each decorator can extract its own value from the return object using its symbol key or custom valueKey option.

How It Works:

  1. Each decorator has a default symbol key (e.g., ATTRIBUTE_METADATA_KEY, PROPERTY_METADATA_KEY)
  2. When a method returns an object, each decorator checks if its key exists in the object
  3. If the key exists, the decorator uses that value; otherwise uses the entire return value
  4. This allows multiple decorators to extract different values from the same return object

Method 1: Using Default Symbol Keys

import {
  ATTRIBUTE_METADATA_KEY,
  PROPERTY_METADATA_KEY,
  STYLE_METADATA_KEY,
  CLASS_METADATA_KEY
} from '@dooboostore/simple-web-component';

@elementDefine('multi-decorator-example')
class MultiDecoratorExample extends HTMLElement {
  @setAttribute('selector')
  @setProperty('selector')
  @updateStyle()
  @updateClass()
  handleUpdate() {
    return {
      [ATTRIBUTE_METADATA_KEY]: 'attribute-value',
      [PROPERTY_METADATA_KEY]: 'property-value',
      [STYLE_METADATA_KEY]: { color: 'red', fontSize: '16px' },
      [CLASS_METADATA_KEY]: { 'active': true, 'disabled': false }
    };
  }
}

Method 2: Using Custom valueKey Option

For more readable code, use the valueKey option to specify custom keys:

@elementDefine('custom-key-example')
class CustomKeyExample extends HTMLElement {
  @setAttribute('selector', { valueKey: 'attrValue' })
  @setProperty('selector', { valueKey: 'propValue' })
  @updateStyle({ valueKey: 'styleValue' })
  @updateClass({ valueKey: 'classValue' })
  handleUpdate() {
    return {
      attrValue: 'attribute-value',
      propValue: 'property-value',
      styleValue: { color: 'red', fontSize: '16px' },
      classValue: { 'active': true, 'disabled': false }
    };
  }
}

Method 3: Mixing Symbol Keys and Custom Keys

You can mix both approaches in the same method:

@elementDefine('mixed-keys-example')
class MixedKeysExample extends HTMLElement {
  @setAttribute('selector')  // Uses default ATTRIBUTE_METADATA_KEY
  @setProperty('selector', { valueKey: 'customProp' })  // Uses custom key
  @updateStyle({ valueKey: 'styles' })  // Uses custom key
  handleUpdate() {
    return {
      [ATTRIBUTE_METADATA_KEY]: 'attr-value',
      customProp: 'prop-value',
      styles: { color: 'blue' }
    };
  }
}

Supported Decorators with valueKey:

  • @applyAttribute / @setAttribute
  • @applyProperty / @setProperty
  • @applySlot / @clearSlot / @appendHtmlSlot / etc.
  • @applyNode / @replaceChildrenNode / etc.
  • @applyStyle / @updateStyle / etc.
  • @applyClass / @updateClass / etc.
  • @emitCustomEvent / @emit
  • @publishSwcAppMessage / @publish

Each decorator will use only its corresponding value from the return object, preventing conflicts and allowing clean separation of concerns.


13. Accommodation Pattern

Factory-based component registration with explicit DI.

// Component factory
export default (w: Window) => {
  const tagName = 'my-component';
  const existing = w.customElements.get(tagName);
  if (existing) return tagName;

  @elementDefine(tagName, { window: w })
  class MyComponent extends w.HTMLElement {
    @onInitialize
    onconstructor(@inject(MyService.SYMBOL) service: MyService) {
      this.service = service;
    }
  }

  return tagName;
};

// Boot factory
export default (w: Window, container: symbol) => {
  serviceFactories.forEach(s => s(container));
};

// Entry point
const container = Symbol('app');
await defineSwcAppBody(w);
const appElement = w.document.querySelector('#app') as SwcAppInterface;
appElement.connect({
  container,
  window: w,
  onStartedLazyDefineComponent: [...pageFactories, ...componentFactories],
  onEngineStarted: () => {
    appElement.innerHTML = '<root-router></root-router>';
  }
});

🔄 Decorator API - Consolidated Pattern (v1.0.43+)

Simplified Decorator API

The decorator API uses a consolidated pattern where all decorators support optional selector parameter that defaults to $this when omitted. This eliminates redundant *This functions.

Current Usage Pattern

// Single function with optional selector
@applyClass('selector', 'update')
method() { ... }

@applyClass('update')  // Selector defaults to $this
method() { ... }

@applyClass  // Bare decorator - selector defaults to $this
method() { ... }

Decorator Functions (Current API)

applyClass.ts:

  • applyClass(selector?, options?) - Set/update/add/remove/toggle classes
  • setClass(selector?, options?) - Replace all classes
  • updateClass(selector?, options?) - Toggle classes
  • addClass(selector?, options?) - Add classes
  • removeClass(selector?, options?) - Remove classes
  • toggleClass(selector?, options?) - Toggle classes

applyStyle.ts:

  • applyStyle(selector?, options?) - Set/update/remove styles
  • setStyle(selector?, options?) - Clear and set styles
  • updateStyle(selector?, options?) - Update/merge styles
  • removeStyle(selector?, options?) - Remove specific styles

applyProperty.ts:

  • property(selector?, options?) - Get/set element properties
  • setProperty(selector?, options?) - Set element properties

applyAttribute.ts:

  • attribute(selector?, options?) - Get/set element attributes
  • setAttribute(selector?, options?) - Set element attributes

query.ts:

  • query(selector?, options?) - Query single element (supports $this, $host, $appHost, etc.)

queryAll.ts:

  • queryAll(selector?, options?) - Query multiple elements

emitCustomEvent.ts:

  • emitCustomEvent(target, type, options?) - Emit custom events
  • emit(target, type, options?) - Short alias

Usage Examples

@elementDefine('my-component')
class MyComponent extends HTMLElement {
  // Class management - all default to $this
  @updateClass()
  toggleActive() {
    return { 'active': this.isActive };
  }

  // Style management - all default to $this
  @updateStyle()
  applyTheme() {
    return { color: this.theme.color, fontSize: '16px' };
  }

  // Property binding - all default to $this
  @setProperty()
  updateValue() {
    return this.computedValue;
  }

  // Attribute binding - all default to $this
  @setAttribute()
  updateId() {
    return this.elementId;
  }

  // Query elements
  @query('#input')
  inputElement?: HTMLInputElement;

  @queryAll('li')
  listItems?: HTMLLIElement[];

  // Emit custom events
  @addEventListener('button', 'click')
  @emitCustomEvent('$this', 'item-selected')
  onItemSelect() {
    return { itemId: this.selectedId };
  }

  // With selector
  @updateClass('.card', 'update')
  updateCardClass() {
    return { 'highlighted': true };
  }
}

Key Features

  1. Reduced API Surface - Consolidated decorators eliminate redundant *This functions
  2. Cleaner Code - Single function name with multiple overloads
  3. Better Discoverability - Intuitive parameter patterns
  4. Flexible Usage - Supports bare decorators, with options, and with selectors
  5. Type Safe - Full TypeScript support with proper type inference

⚠️ Critical Rules

DO NOT Use @Sim on Web Components

@sim is for Services only. Web Components should use @elementDefine.

// ✅ CORRECT
@sim
export class UserService { }

@elementDefine(tagName, { window: w })
class UserWidget extends w.HTMLElement { }

// ❌ WRONG
@sim
@elementDefine(tagName, { window: w })
class UserWidget extends w.HTMLElement { }

Factory Always Returns

export default (w: Window) => {
  const tagName = 'my-element';
  const existing = w.customElements.get(tagName);
  if (existing) return existing;  

  @elementDefine(tagName, { window: w })
  class MyElement extends w.HTMLElement { }
  
  return existing; 
};

@onInitialize for DI (Not Constructor Parameters)

// ✅ CORRECT
@onInitialize
onconstructor(@inject(Service.SYMBOL) service: Service) {
  this.service = service;
}

// ❌ WRONG
constructor(private service: Service) { super(); }  // Web Components can't have constructor parameters!

📚 Examples


🔗 Related Packages


📄 License

MIT License