@dooboostore/dom-render

NPM versionBuild and TestLicense: MIT

A reactive and component-oriented DOM template engine for fine-grained rendering.@dooboostore/dom-rendertracks state changes through proxy-based observation and updates only affected render units.


Features

  • Reactive object proxy rendering (DomRender,DomRenderProxy)
  • HTML template bindings (${...}$,#...#,@this@)
  • Structural directives (dr-if,dr-for,dr-for-of,dr-repeat,dr-strip,dr-appender)
  • Event directives (dr-event-*,dr-window-event-*,dr-on-init,dr-on-rendered-init)
  • Form directives (dr-formand validator integration)
  • Component system (ComponentBase,createComponent, target elements/attrs)
  • Lifecycle hooks (onCreateRender,onInitRender,onRawSetRendered, etc.)
  • Built-in messenger for inter-component pub/sub
  • Router integration (Path/Hash) via config

Installation

pnpm add @dooboostore/dom-render# ornpm install @dooboostore/dom-render

Quick Start

import{DomRender}from'@dooboostore/dom-render';constapp =document.querySelector('#app')!;letstate = {title:'Hello DomRender',count:0,increment() {this.count+=1;
  }
};

app.innerHTML=`
  <h1>${@this@.title}$</h1>
  <p>Count:${@this@.count}$</p>
  <button dr-event-click="@this@.increment()">+1</button>
`;constresult =newDomRender({rootObject: state,target: app,config: {window}
});

state = result.rootObject;

Public API (Root Export)

@dooboostore/dom-render's root entry (src/index.ts) re-exports as flat named exports — there is nocomponents/configs/etc. namespace object to import:

import{DomRender,DomRenderProxy,ComponentBase, attribute,Appender,DomRenderConfig}from'@dooboostore/dom-render';

This covers: a selected set of built-in components/helpers (If,This,AppenderComponent,PromiseSwitch,Choose,CheckBox,Radio,Select,Timer,Details,Input,Route,RouterOutlet,A,ComponentSet,ComponentRouterBase,ComponentBase,attribute, and the combineddrComponentmap used to register them all at once), plus everything fromconfigs,decorators,events,lifecycle,messenger,operators(including theAppendercollection class),rawsets,types,DomRenderandDomRenderProxy.

Thequeryandeventdecorators arenotre-exported from the root; import them via the./*subpath export:

import{ query, event }from'@dooboostore/dom-render/components/ComponentBase';

Core Concepts

1) Reactive Root Object

DomRenderwraps your root object with a proxy. Mutating fields onresult.rootObjecttriggers reactive rendering.

constresult =newDomRender({rootObject: {name:'kim',age:20},target: element,config: {window}
});constroot = result.rootObject;
root.age=21;// dependent template fragments update

2) Template Expressions

  • ${expr}$: evaluate JS expression in render context
  • #it#,#nearForOfIndex#: loop context placeholders
  • @this@: current render/component instance pointer
<div>${@this@.user.name}$</div><lidr-for-of="@this@.items">${#it#.title}$</li>

3) Incremental Render Units (RawSet)

DOM fragments are segmented into units with dependency tracking. When path changes, only connected units are re-executed.


Structural Directives

dr-if

Conditional rendering based on expression truthiness.

<divdr-if="@this@.isLoggedIn">Welcome, ${@this@.user.name}$</div>

dr-for

Classicfor(...)loop.dr-option-itis evaluated per iteration and its value is substituted for#it#in the cloned markup.

<lidr-for="let i=0; i<@this@.items.length; i++"dr-option-it="i">${@this@.items[#it#].name}$</li>

dr-for-of

Array/object iteration with#it#context replacement.

<ul><lidr-for-of="@this@.items">${#it#.name}$</li></ul>

dr-repeat

Repeat block by count/range expression.

<divdr-repeat="@this@.columns">Cell ${#it#}$</div>

dr-appender

Optimized append/update/delete for list-like incremental data.

<ul><lidr-appender="@this@.rows">${#it#}$</li></ul>

dr-strip/dr-option-strip

Strip wrapper element while preserving children.

<divdr-if="@this@.ok"dr-option-strip="true">#innerHTML#</div>

Template Utilities

Dynamic content directives

  • dr-inner-html: bindinnerHTML

  • dr-inner-text: bindinnerText

  • dr-this,dr-this-property: bind target context object

  • dr-option-before,dr-option-after: pre/post scripts attached alongside another directive on the same element

  • dr-value,dr-src,dr-checked,dr-selected,dr-readonly,dr-disabled,dr-hidden,dr-required,dr-open,dr-class,dr-style: bind the corresponding property/attribute from an expression

  • dr-attr="{key: expr, ...}": shorthand that expands each pair intokey="${expr}$"

Plain attributes can also be bound by putting a template expression inside the attribute value itself:

<divdr-inner-text="@this@.plainText"></div><divdr-inner-html="@this@.trustedHtml"></div><imgsrc="${@this@.imageUrl}$"alt="${@this@.alt}$"/><imgdr-attr="{src: @this@.imageUrl, alt: @this@.alt}"/>

Form utility (dr-form)

dr-formwires every[name]child to the target object (optionally withValidator/ValidatorArrayfrom@dooboostore/core). Per-field options:dr-form:name,dr-form:event(defaultchange),dr-form:validator.

<formdr-form="@this@.formState"><inputname="email"dr-form:name="'email'"dr-form:event="change"/></form>

Appender utility class

Appendersupports keyed incremental collection updates.

import{Appender}from'@dooboostore/dom-render';conststate = {rows:newAppender('A','B')
};

state.rows.set('key-1','Updated A');
state.rows.delete('key-1');
state.rows.clear();

Event System

dr-event-*directive

Bind DOM events declaratively.

<buttondr-event-click="@this@.submit($event)">Submit</button><inputdr-event-input="@this@.onInput($event)"/>

dr-window-event-*directive

Listen to window-scoped events.

<divdr-window-event-resize="@this@.onResize($event)"></div>

EventManager behavior

  • delegated listener strategy for common events
  • direct attachment for non-delegatable events
  • execution context variables:$event,$target, and render-bound objects

Component Architecture

ComponentBase

ComponentBaseprovides:

  • attribute binding (@attribute)
  • query binding (@query)
  • event binding (@event)
  • child component tracking
  • lifecycle integration
  • decorator refresh APIs (refreshDecorators,refreshQueryDecorators,refreshEventDecorators)

@attribute

import{ComponentBase, attribute }from'@dooboostore/dom-render';classCardextendsComponentBase{@attribute('title')
  title ='';@attribute({name:'count',converter:(v) =>Number(v ??0) })
  count =0;
}

@query

import{ComponentBase, query }from'@dooboostore/dom-render';classPanelextendsComponentBase{@query('.title')titleEl?:HTMLElement;@query({selector:'.row',refreshRawSetRendered:true})rows:HTMLElement[] = [];
}

@event

import{ComponentBase, event }from'@dooboostore/dom-render';classToolbarextendsComponentBase{@event({query:'.save-btn',name:'click'})onSave() {console.log('saved');
  }
}

createComponent

import{DomRender}from'@dooboostore/dom-render';classUserCard{
  name ='Anonymous';
}constuserCard =DomRender.createComponent({type:UserCard,tagName:'user-card',template:`<div>${@this@.name}$</div>`});// DomRender.run({...}) is a shorthand that returns the proxied rootObject directly.constapp =newDomRender({rootObject: {users: [] },target:document.querySelector('#app')!,config: {window,targetElements: [userCard] }
});

Lifecycle Hooks

Implement interfaces to receive lifecycle callbacks:

  • onProxyDomRender(config)
  • onCreateRender(...args)/onCreateRendered(...args)
  • onCreateRenderData(data)
  • onCreatedThisChild(child, childData)
  • onInitRender(param, rawSet)
  • onRawSetRendered(rawSet, otherData)
  • onChildRawSetRendered()(interface inlifecycle/OnChildRawSetRendered, not re-exported fromlifecycle/index)
  • onChildRenderedByProperty(key, value)
  • onDestroyRender(params)
  • onChangeAttrRender(name, value, other)
  • onBeforeReturnGet(name, value, fullPath)/onBeforeReturnSet(name, value, fullPath)
classViewModel{onCreateRender() {console.log('render create');
  }asynconInitRender() {console.log('render initialized');
  }
}

Router Integration

DomRendercan create router byrouterType:

  • 'hash'->HashRouter
  • 'path'->PathRouter
  • custom router object/factory
constapp =newDomRender(
  {rootObject: {page:'home'},target:document.querySelector('#app')!,config: {window,routerType:'hash'}
  },
  {firstUrl:'home'}
);

app.router.go('/about');

Messenger

DefaultMessengerandMessengerprovide channel-based communication across render roots/components.

Use this when direct parent-child access is not suitable.


Requested Decorator Mapping

This section is added for teams using bothdom-renderandsimple-web-component.

@event/@addEventListener(extended)

@event(alias of@addEventListener) is a decorator from@dooboostore/simple-web-component; it is unrelated to dom-render's own@event({ query, name }).

Indom-render, equivalent patterns are:

  1. Template directive:dr-event-*
  2. Class decorator in component:@event({ query, name })
<buttondr-event-click="@this@.handleClick($event)">Click</button>
classMyCompextendsComponentBase{@event({query:'.btn',name:'click'})onClick(e:Event) {}
}

@replaceChildren(extended)

@replaceChildrenis fromsimple-web-component.

dom-renderalternatives:

  1. structural replacement via directives (dr-if,dr-for-of,dr-appender)
  2. explicit DOM strategy in component methods (replaceChildren) when needed
updateRows(container:HTMLElement,nodes:Node[]) {
  container.replaceChildren(...nodes);
}

For reactive templates, prefer declarative updates over manual replacement.

@insertBeforeEnd(extended)

simple-web-componentappends via@insertBeforeEnd(there is no@appendChilddecorator).

dom-renderalternatives:

  1. dr-appender+Appenderfor incremental append/delete
  2. directappendChildin imperative setup code
state.rows.set('k1','first row');// reactive append through dr-appender

Best Practices

  1. Always mutateresult.rootObject(proxied object), not the original plain object.
  2. Prefer declarative directives over direct DOM mutation.
  3. Usedr-appenderfor large frequently changing collections.
  4. For dynamic component trees, callrefreshDecorators()when bindings need explicit refresh.
  5. UseDomRenderNoProxyfor fields that should never trigger proxy traversal.
  6. Keep heavy computations outside template expressions.

Troubleshooting

UI not updating after mutation

  • Check that you are mutatingresult.rootObject, not stale reference.
  • Verify expression paths are valid (@this@.x.y).

Event not firing

  • Confirm directive name (dr-event-click,dr-event-input, etc.).
  • Check script context variables ($event,$target).

Render loop/performance drop

  • Avoid expensive script evaluation in template.
  • Prefer keyedAppenderupdates instead of full array replacement.

License

This package is licensed under theMIT License.