MCPcopy Index your code
hub / github.com/dom-native/dom-native

github.com/dom-native/dom-native @v0.11.3

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.11.3 ↗ · + Follow
355 symbols 776 edges 44 files 9 documented · 3%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

DOM-Native: A Native Web Component TypeScript Optimized Utility

dom-native is a minimalistic utility (<7kb gzip) embracing the DOM Native Web Components model to build simple to big applications the DOM way. #LearnWhatMatters #SimpleScaleBetter

THE DOM IS THE FRAMEWORK

QUICK DEMO

YouTube tutorials: Native Web Components | Quickstart introduction

Key features and approach:

  • REAL DOM (i.e., No Virtual DOM): Fully embrace native DOM customElement and Web Components.

  • Modern (i.e., ZERO IE TAX): Only target modern browsers (e.g., Chrome, Edge Chromium, Firefox, and Safari). NO Polyfill or Shiming.

  • JUST A LIB NOT a framework, the Browser is the Framework (uses the native DOM customElement / webcomponent as the framework).

  • SMALL <7kb gzipped (< 18kb minimized) and ZERO dependency!

  • SIMPLE base class providing expressive lifecycle by hooking to native DOM custom elements

  • e.g., BaseHMLElement extends HTMLElement with .init .preDisplay .postDisplay.

  • O(1) event binding support by fully utilizing DOM event bubbling with optional namespacing

on(containerEl, 'pointerup', '.my-div', (evt) => { 
    console.log('clicked')
}, {ns: 'some_namespace'});
// or can namespace "a la jquery": on(containerEl, 'pointerup.some_namespace', ...)

// ...

off(containerEl, {ns: 'some_namespace'});
  • TYPED for expressiveness and robustness with some minimalistic but powerfull TS decorators
import {customElement, BaseHTMLElement, onEvent} from 'dom-native';

@customElement('my-element') // shorthand for customElements.define('my-element', MyElement)
class MyElement extends BaseHTMLElement{
  @onEvent('pointerup', '.big-button') // bind on this element, and call if match '.big-button'
  bigButtonClick(evt) { ... }

  @onDoc('pointerup', '.main-menu') // bind on document, will unbind on disconnected
  mainMenuClicked(evt) { ... }  
  // document binding will be removed on HTMLElement detached
}
  • LIGHT and expressive DOM API wrappers (e.g., first(el, selector) all(el, selector))
  • e.g., const itemsEl = first(el, 'ul.items'); all(itemsEl,'li').map(liEl => console.log(liEl))

  • PUB/SUB - Unleash state management with a Minimalistic pub/sub api (see below)

  • AGNOSTIC - NO templating included. Feel free to use template literals, handlebars, or lit-html.

IN SHORT - Simple Scales Better - Learn what matters - favor pattern over frameworks - The DOM is the Framework! - Real DOM is Back!!!

Hello World

npm install dom-native

BaseHTMLElement is a simple class that extends the browser native HTMLElement and provides expressive binding mechanisms and lifecycle methods.

Here an example with some TypeScript decorators (can be used without TypeScript as well)

// main.ts
import { BaseHTMLElement, onEvent } from 'dom-native';

@customElement('hello-world') // no magic, just call customElement.register('hello-world',HelloComponent); 
class HelloComponent extends BaseHTMLElement{

  get name() { return this.getAttribute('name') || 'World'}

  @onEvent('click', 'strong') // bind a DOM event from this element instance with a selector
  onStrongClick(){
    console.log(`${this.name} has been clicked`);
  }

  // init() - will be called only once at first connectedCallback 
  init(){
    this.innerHTML = `Hello <strong>${this.name}</strong>`;
    // or shadowDom, DocumentFragment, ....
  }

  // called just before the first paint.
  preDisplay() {
    // put here thing job that needs to be performed just before rendering and could be in init.
  }

  // postDisplay() - will be called at the second requestFrameAnimation (after first paint)
  postDisplay(){
    // some async and UI update/refresh
  }
}

document.body.innerHTML = '<hello-world name="John"></hello-world>';

HTML will be:

<body><hello-world name="John">Hello <strong>John</strong></hello-world></body>

Fully based on Native Web Component (customElements) with a lightweight but powerful BaseHTMLElement extends HTMLElement base class with some simple and highly productive DOM APIs allowing to unparallel productivity to build simple to big Web frontends.

dom-native is designed to scale from the get-go and, therefore, fully embrace TypeScript types and makes a lightweight, expressive, and optional use of TypeScript decorators (JS decorator standard is still way off). However, all functionalities are available in pure JS as well.

Full BaseHTMLElement lifecycle and typescript decorators

dom-native BaseHTMLElement is just a base class that extends DOM native custom element class HTMLElement and add some minimalistic but expressive lifecycle methods as well as few typescript decorators (or properties for pure JS) to allow safe event bindings (i.e., which will get unbound appropriately).

// full component
import { BaseHTMLElement, onEvent, onDoc, onWin, onHub } from 'dom-native';

@customElement('full-component')
class FullComponent extends BaseHTMLElement{

  //// DOM Event Bindings
  // bind a method to an element DOM event with an optional selector
  @onEvent('click', 'strong') 
  onStrongClick(){
    console.log('World has been clicked');
  }

  // bind a method to an document DOM event with an optional selector
  // Note: will be correctly unbound when this element is removed from the Document
  @onDoc('click', '.logoff')
  onDocumentLogoff(){
    console.log('.logoff element was clicked somewhere in the document');
  }

  // bind a method to an window DOM event
  // Note: will be correctly unbound when this element is removed from the Document
  @onWin('resize')
  onDocumentLogoff(){
    console.log('Window was resized');
  }

  //// Hub Event Bindings
  @onHub('HUB_NAME', 'TOPIC_1', 'LABEL_A')
  onPubSubTopic1LabelAEvent(message: any, info: HubEventInfo){
    console.log(`hub event message: ${message}, for topic: ${info.topic}, label: ${info.label}`);
  }

  //// Lifecyle Methods
  // init() - will be called only once at first connectedCallback 
  init(){
    super.init(); // best practice, call parent
    this.innerHTML = 'Hello <strong>World</strong>'; // good place to set the innerHTML / appendChild
  }

  // preDisplay() - if defined, will be called once before first paint (i.e. first requestAnimationFrame)
  preDisplay(){ 
    console.log('before first paint');
    // Note: This function can be marked async, but the first paint won't wait until resolve. 
    //       More idiomatic to have async in postDisplay
  }

  // postDisplay() - if defined, will be called once after first paint, before second paint (i.e., second requestAnimationFrame)
  async postDisplay(){
    // good place to do some async and UI update/refresh
  }

  // Native custom element method on "remove". 
  disconnectedCallback(){
    super.disconnectedCallback(); // MUST CALL for bindings (i.e., @onDoc, @onWin, @onHub) cleanup
    // other cleanups as needed
  }

  // Native custome element method
  attributeChangedCallback(attrName: string, oldVal: any, newVal: any){
    // Implement when/as needed.
  }


}

To fully understand BaseHTMLElement lifecycle, which is just an extension of the browser native HTMLElement one, it is essential to understand the nuances of the native HTMLElement lifecycle.

Understanding DOM customElement lifecycle

See MDN documation about custom element lifecycle callbacks

In short, there are - First you associate a class which extends HTMLElement with a tag name using customElements.define('my-comp', 'MyComponent') - dom-native provides an optional convenient @customElement('my-comp') TypeScript decorator - Then, the class can define the following methods: - connectedCallback: Invoked each time the custom element is appended into a document-connected element. Each time the node is moved, this may happen before the element's contents have been fully parsed. - disconnectedCallback: Invoked each time the custom element is disconnected from the document's DOM. - attributeChangedCallback: Invoked each time one of the custom element's attributes is added, removed, or changed. Which attributes to notice change for is specified in a static get observedAttributes method - There is a adoptedCallback, but it is used mostly in the iframe case, which is not common.

With this in mind, here are the BaseHTMLElement extended lifecycle methods based on the native HTMLELement ones.

@customElement('my-comp') // no magic, just call a customElement.register('my-comp',MyComponent);
class MyComponent extends BaseHTMLElement{
  private _data?: string;

  set data(data: string){ this._data = data; }
  get data(){ return this._data }

  constructor(){
    super(); // MUST, by DOM customElement spec
    console.log('-- constructor', this.data);
  }
  init(){
    super.init(); // not necessary, but good best practice
    console.log('-- init ', this.data);
  }

  preDisplay(){
    console.log('-- preDisplay', this.data);
  }

  postDisplay(){
    console.log('-- postDisplay', this.data);
  }  
}

const el = document.createElement('my-comp'); 
// NOTHING is printed, the element is not added to the dom. 
// DO NOT call `el.customData = 'test data'` MyComponent is not instantiated

document.body.appendChild(el);
// -- print --> '-- constructor undefined'
// -- print --> '-- init undefined'

// now 'el' has been upgraded to MyComponent instance
el.customData = 'test-data-1'; 

document.addEventListener('DOMContentLoaded', function () {
  const el2 = document.createElement('my-comp'); 
  // NOTE: Since called after the DOMContentLoaded, MyComponent got instantiated
  // -- print --> '-- constructor undefined'
  // -- print --> '-- init undefined'  
  el2.data = 'test-data-2'; // works, as the setter/getter got instantiated
}); 

// this will ask the browser to do a call back before first paint, but it will do after preDisplay, because the myComponent instance was created before
requireAnimationFrame(function(){

  // -- print --> '-- preDisplay test-data-1'
  // -- print --> '-- preDisplay test-data-2'

  // this will ask the browser to register a callback for the upcoming paint (which is the one after this one).
  // Since MyComponent had a postDisplay, it was also registered with a double requireAnimationFrame and will be called before
  requireAnimationFrame(function(){

    // -- print --> '-- postDisplay test-data-1'
    // -- print --> '-- postDisplay test-data-2'

  });
});

NOTE - element upgrade

There is an essential detail on when the component class MyComponent gets associated with its tag my-comp, and it follows the following "element upgrade" rule. - When using document.createElement('my-comp') if the MyComponent associated class was defined before, then it will get immediately 'upgraded', meaning that the returned value will be of type MyComponent - However, if when using document.createElement('my-comp') the MyComponent was not defined, or if the <my-comp></my-comp> was created via a template (i.e. in a document fragment), then the MyComponent will get instantiated and associated to the tag my-comp when the element is added to the document.body DOM tree. This is important if the MyComponent has some setters/getters, as they won't be defined until after the DOM is added to the body.

Best Practices

Here are three typical rendering scenarios:

1) Attribute / Content Rendering

If the component can infer its content soly from its declaration (i.e., attributes and content), then, set the innerHTML or call appendChild in the init() method. Favor this.innerHTML or one this.appendChild call (e.g., using the convenient frag('<some-html>text</some-html>) dom-native DocumentFragment builder function)

@customElement('ami-happy') 
class AmIHappy extends BaseHTMLElement{
  init(){
    super.init(); // optional, but good best practice
    const happy = this.hasAttribute('happy');
    this.innerHTML = `I am ${happy ? '<strong>NOT</strong>' : ''} happy`;
  }
}
const el = document.createElement('ami-happy');
document.body.appendChild(el);

// -- display --> <ami-happy>I am <strong>NOT</strong> happy</ami-happy>

2) Data Initialization Rendering

If the component needs more complex data structure to render itself, but those data do not require any async, adding the component to the document to instantiate the component, and then, calling the data initializers will allow the preDisplay() to render those data before first paint.

@customElement('whos-happy') 
class WhosHappy extends BaseHTMLElement{
  data?: {happy: string[], not: string[]}

  preDisplay(){
    if (this.data){
      this.innerHTML = `Happy people: ${this.happy.join(', ')} 


                      Not happy: ${this.not.join(', ')}`;
    }
  }
}

// create element and add it to the DOM
const el = document.createElement('whos-happy');
const whosHappyEl = document.body.appendChild(el) as WhosHappy;

whosHappyEl.data = {happy: ['John', 'Jen'], not: ['Mike']}; // <-- still before first paint, so NO flicker

// -- display --> <whos-happy>Happy people: John, Jen 

 Not happy: Mike</whos-happy>

// Note: If we had done it at the init(), this.data would have been undefined.

3) Async Rendering

When a component needs to get data asynchronously, then, async postDisplay() method is a good place to put this logic, and usually the component HTML structure gets assigned at the init() method.

Extension points exported contracts — how you extend this code

Hub (Interface)
(no doc) [2 implementers]
src/hub.ts
HTMLElementTagNameMap (Interface)
(no doc)
test/src/test-dom.ts
PositionOptions (Interface)
(no doc)
src/position.ts
OnEvent (Interface)
(no doc)
src/event.ts
HTMLElementTagNameMap (Interface)
(no doc)
demo/src/spec-anim.ts
NsObject (Interface)
(no doc)
src/hub.ts
RunController (Interface)
(no doc)
test/src/runner.ts
RefPositionOptions (Interface)
(no doc)
src/position.ts

Core symbols most depended-on inside this repo

equal
called by 125
test/src/utils.ts
first
called by 63
src/dom.ts
customElement
called by 22
src/ts-decorator-custom-element.ts
on
called by 22
src/event.ts
append
called by 21
src/dom.ts
elem
called by 20
src/dom-builders.ts
html
called by 19
src/dom-builders.ts
hub
called by 14
src/hub.ts

Shape

Function 204
Class 73
Method 63
Interface 15

Languages

TypeScript100%

Modules by API surface

demo/src/spec-perf.ts40 symbols
test/src/test-base.ts33 symbols
src/hub.ts31 symbols
test/src/test-dom.ts24 symbols
test/src/test-event.ts20 symbols
src/utils.ts13 symbols
test/src/test-css.ts12 symbols
src/event.ts12 symbols
src/dom.ts12 symbols
demo/src/spec-shadow-dom.ts12 symbols
demo/src/spec-event.ts12 symbols
demo/src/spec-basic.ts11 symbols

For agents

$ claude mcp add dom-native \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact