Skip to content

Repository files navigation

Gitpod ready-to-code

Drow.js 🎭

The Tiny, Object-Based Web Component Library.

Drow is a minimalist wrapper for the Web Components API. It replaces the boilerplate of JavaScript Classes with a clean, object-based configuration. Define your components as simple objects and let Drow handle the registration and rendering.

Why Drow?

  • 🚫 Zero Dependencies: No NPM, no build steps, no headaches.
  • 📉 Microscopic Size: Tiny footprint with high performance.
  • 🧩 Object-First API: No more class X extends HTMLElement or super().
  • Native Performance: Uses the browser's built-in Custom Elements registry.
  • 🔁 Batched Rendering: Rapid state mutations are coalesced into a single DOM update per frame.

Drow - Simple Web Component Library for creating custom HTML Components.

Setup

Include drow.js in your HTML file:

<scriptsrc="drow.js"></script>

Or as an NPM module:

npm install drowjs
importDrowfrom'drowjs';

Define a Drow Component

Drow components are defined using simple objects. Reactivity is handled automatically via the state object and methods.

constconfig={name: "my-counter",state: {count: 0},css: `button { font-weight: bold; }`,template: ` <div> <button @click="increment">Count is {{count}}</button> </div> `,methods: {increment(){this.state.count++;}}};Drow.register(config);

Config Reference

KeyTypeDescription
namestringRequired. Custom element tag name — must contain a hyphen.
templatestringHTML template string. Supports {{expression}} interpolation.
stateobjectReactive per-instance state. Changes trigger a batched re-render.
methodsobjectEvent handler functions. Called with this bound to the component.
computedobjectDerived values: { key: (state) => value }. Available in templates.
propsstring[]Observed HTML attributes, exposed as template variables.
cssstringScoped CSS. Use :host to target the component root.
shadowbooleanAttach a Shadow DOM instead of rendering in the Light DOM.
useStorebooleanSubscribe to the global store. Access values via {{store.key}}.
initfunctionLifecycle hook — called once after the component connects.
disconnectedfunctionLifecycle hook — called when the component is removed from the DOM.
updatedfunctionLifecycle hook — called after every render cycle.
watchfunction(attr)Called when an observed prop attribute changes. Receives { name, oldValue, newValue, comp }.

Directives Reference

DirectiveDescriptionExample
@eventBind DOM events. Supports .prevent and .stop modifiers.@click="doSomething"
@event.preventCalls preventDefault() before the handler.@submit.prevent="save"
@event.stopCalls stopPropagation() before the handler.@click.stop="select"
d-modelTwo-way binding for inputs. Supports nested paths.d-model="user.name"
d-forRender a list. Supports objects and (item, index) syntax.d-for="item in items"
d-keyKeyed diffing for d-for — patches only changed items.d-key="{{item.id}}"
d-ifConditional rendering — removes element from DOM.d-if="isVisible"
d-else-ifChained condition after d-if.d-else-if="otherFlag"
d-elseFallback block after d-if or d-else-if.d-else
d-showToggles display: none — element stays in DOM.d-show="isVisible"
d-class:nameConditionally adds/removes a CSS class.d-class:active="isActive"
d-bind:attrReactive attribute binding.d-bind:src="imageUrl"
d-htmlSets innerHTML from a state value. Use with trusted content only.d-html="richContent"
d-refRegisters element in this.refs for direct DOM access.d-ref="myInput"
d-cloakHidden until the component finishes rendering (prevents FOUC).d-cloak

Interpolation

Use {{key}} in templates to render state, computed, prop, or store values. Dot notation is supported for nested access.

<p>{{user.profile.name}}</p><p>{{store.currentUser}}</p>

Event Binding

Bind any DOM event with @eventName="methodName". Modifiers can be chained with .:

<button@click="save">Save</button><form@submit.prevent="handleSubmit">...</form><div@click.stop="select">...</div>

Two-Way Binding (d-model)

d-model syncs an input's value with state. It supports nested paths:

<inputd-model="username" /><inputd-model="address.city" /><inputtype="checkbox" d-model="settings.darkMode" />

List Rendering (d-for)

Iterate over arrays of primitives or objects:

<!-- Primitive list --><lid-for="tag in tags">{{tag}}</li><!-- Object list --><lid-for="user in users">{{user.name}} — {{user.email}}</li><!-- With index --><lid-for="(item, index) in items">{{index}}. {{item}}</li>

Keyed Diffing (d-key)

Add d-key to enable efficient patching — Drow will reuse existing DOM nodes instead of rebuilding the entire list on every render:

<lid-for="item in items" d-key="{{item.id}}">{{item.name}}</li>

Use a value that uniquely identifies each item (e.g. an id). Without d-key, the list is rebuilt from scratch on each render.


Conditional Rendering (d-if / d-else-if / d-else)

<pd-if="status === 'loading'">Loading…</p><pd-else-if="error">Something went wrong.</p><pd-else>Ready.</p>

d-else-if and d-else must immediately follow a d-if (or another d-else-if) element with no elements between them.

Use d-show instead of d-if when the element should remain in the DOM and only its visibility should toggle:

<divd-show="isPanelOpen">...</div>

Computed Properties

Computed values derive from state and are re-evaluated on every render:

Drow.register({name: "cart-summary",state: {items: []},computed: {total: (state)=>state.items.reduce((sum,item)=>sum+item.price,0),isEmpty: (state)=>state.items.length===0},template: ` <div> <p d-if="isEmpty">Your cart is empty.</p> <p d-else>Total: ${{total}}</p> </div> `});

Lifecycle Hooks

Drow.register({name: "live-clock",state: {time: ""},init(){// Called once after the component connects to the DOMthis._timer=setInterval(()=>{this.state.time=newDate().toLocaleTimeString();},1000);},disconnected(){// Called when the component is removed from the DOMclearInterval(this._timer);},updated(){// Called after every render cycleconsole.log("clock re-rendered:",this.state.time);},template: `<span>{{time}}</span>`});

Global Store

Share state across multiple components with Drow.store. Any component with useStore: true re-renders when the store changes.

// WriteDrow.store.state.user="Alice";// Read in template// {{store.user}}
Drow.register({name: "user-badge",useStore: true,template: `<span>Logged in as {{store.user}}</span>`});Drow.register({name: "login-btn",useStore: true,methods: {login(){Drow.store.state.user="Alice";}},template: `<button @click="login">Log In</button>`});

Custom Events (this.emit)

Components can dispatch custom events that bubble up the DOM:

methods: {submit(){this.emit("form-submit",{data: this.state.formData});}}

Listen from a parent:

document.querySelector("my-form").addEventListener("form-submit",(e)=>{console.log(e.detail.data);});

Refs (d-ref)

Access DOM elements directly via this.refs:

<inputd-ref="emailInput" type="email" /><button@click="focusEmail">Focus</button>
methods: {focusEmail(){this.refs.emailInput.focus();}}

Scoped CSS

CSS defined in css is automatically scoped to the component. Use :host to target the component element itself:

css: ` :host { display: block; padding: 1rem; } button { background: #a78bfa; color: white; }`

For Shadow DOM components (shadow: true), standard :host CSS works natively.


Props & Attribute Watching

Declare observed attributes with props. They're available as template variables and trigger a re-render when changed. Use watch to respond to changes:

Drow.register({name: "user-card",props: ["username","avatar"],watch({ name, oldValue, newValue }){console.log(`${name} changed: ${oldValue}${newValue}`);},template: ` <div> <img d-bind:src="avatar" /> <p>{{username}}</p> </div> `});
<user-cardusername="Alice" avatar="alice.png"></user-card>

Debug Mode

Drow.debug controls console output. It defaults to true. Set it to false before registering components in production to suppress all Drow logs and dev warnings:

Drow.debug=false;Drow.register({ ... });

In debug mode, Drow will warn you if:

  • A component name is missing or doesn't contain a hyphen (required by the Custom Elements spec)
  • An unknown key is present in a config object

Interactive Demo: Todo List

This example demonstrates list rendering, two-way data binding, and event handling.

Drow.register({name: "todo-app",state: {newTask: "",tasks: ["Master Drow.js","Build a tiny app"]},template: ` <div class="todo-box"> <h3>Task List ({{count}})</h3> <input d-model="newTask" placeholder="Add a new task..."> <button @click="addTask">Add</button> <button @click="clearTasks">Clear All</button> <ul> <li d-for="(task, index) in tasks" d-key="{{task}}"> <span>{{index}}. {{task}}</span> <button @click="removeTask" data-item="{{task}}">x</button> </li> </ul> </div> `,computed: {count: (state)=>state.tasks.length},methods: {addTask(){if(this.state.newTask.trim()){this.state.tasks=[...this.state.tasks,this.state.newTask];this.state.newTask="";}},removeTask(e){constitem=e.target.dataset.item;this.state.tasks=this.state.tasks.filter(t=>t!==item);},clearTasks(){this.state.tasks=[];}},css: ` .todo-box { border: 1px solid #444; padding: 1rem; border-radius: 8px; } input { padding: 5px; border-radius: 4px; border: 1px solid #ccc; } button { cursor: pointer; background: #a78bfa; color: white; border: none; padding: 5px 10px; border-radius: 4px; } li { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; } li button { background: #ef4444; padding: 2px 8px; } `});

Examples

Check out the live interactive demos:


Documentation

Detailed API documentation is generated using JSDoc. To generate it locally:

npm run doc

The documentation is generated in the docs/ directory.


Local Development

npm install
npm run server

Building

npx terser drow.js -o drow.min.js --compress --mangle

Credits

Author johnfacey.dev

About

Web Component Wrapper for creating HTML Custom Components

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages