@tmrw/component v0.0.4
@tmrw/components
A friendly yet powerful plugin for TomorrowJS that adds a flexible, HTML-first component system to your web projects. If you've ever wanted to reuse bits of dynamic UI without adopting a heavy framework, @tmrw/components is your solution. It stays true to TomorrowJS's minimal philosophy—no virtual DOM, no custom templating—just plain HTML, a light store, and a few life-cycle hooks.
Why Use @tmrw/components?
HTML-First, No Overbearing Tooling
Write everyday HTML, like<div data-t-component="my-counter">
, with no additional build step or template language.Minimal & Familiar
Under the hood, it's just the TomorrowJS store, DOM elements, and straightforward subscription logic—no big mental shift if you already know TomorrowJS.Lifecycle Hooks & Reactive State
Gain simple reactivity (dependsOn
for selective re-renders) and lifecycle events (onMount
,onUnmount
) for smoother, more maintainable UIs.Nested Components
Let components render child components in the same HTML, and watch them automatically mount themselves.Cross-Component Communication
Usedata-t-key="someKey"
so you can grab a component anywhere withgetComponentByKey("someKey")
—ideal for dashboards, widgets, or advanced flows.Safe & Type-Friendly
Includes asafeSubscribe
wrapper to avoid TypeScript errors if your store doesn't fully conform to the ideal() => void
unsubscribe signature.
Table of Contents
- Installation
- Basic Example
- Defining Components
- Using Components in HTML
- Mounting & Lifecycle
- Keys & Cross-Communication
- Nested Components
- Advanced Patterns
- TypeScript Note
- License
Installation
npm install @tmrw/core @tmrw/components
Or:
yarn add @tmrw/core @tmrw/components
Note: You need
@tmrw/core
(TomorrowJS) as the base library. This plugin extends it.
Basic Example
Below is a quick look at how you might define a simple "counter" component and use it in HTML:
// 1) Import and register the plugin
import { use, initTmrw, createStore, registerAction } from '@tmrw/core';
import { componentsPlugin, defineComponent, mountComponents } from '@tmrw/components';
use(componentsPlugin);
// 2) Define an action so we can increment the store
registerAction('incrementCount', ({ store }) => {
if (!store) return;
const current = store.get('count') ?? 0;
store.set('count', current + 1);
});
// 3) Define a component called "my-counter"
defineComponent({
name: 'my-counter',
createStore: () => createStore({ count: 0 }), // local store with `count`
render(instance) {
const countVal = instance.store.get('count') ?? 0;
return `
<div>
<p>Count: ${countVal}</p>
<button data-t-on="click:incrementCount">Increment</button>
</div>
`;
},
dependsOn: ['count'], // re-render only if 'count' changes
});
// 4) Initialize TomorrowJS & mount any components in #app
initTmrw();
mountComponents({ root: document.getElementById('app') });
HTML:
<div id="app">
<!-- We place our custom component here -->
<div data-t-component="my-counter"></div>
</div>
Defining Components
Use the exported defineComponent()
function:
defineComponent({
name: 'my-slider',
createStore: () => createStore({ value: 50 }),
render(instance) {
const val = instance.store.get('value') ?? 0;
return `
<div>
<input
type="range"
min="0"
max="100"
value="${val}"
data-t-on="input:updateSlider"
/>
<span>${val}</span>
</div>
`;
},
// If you only care about 'value', you can specify:
dependsOn: ['value'],
// Lifecycle hooks:
onMount(instance) {
console.log('Slider mounted:', instance.key);
},
onUnmount(instance) {
console.log('Slider unmounted:', instance.key);
},
});
Key Fields:
name
: Unique string that appears indata-t-component="..."
createStore?()
: Optional. Each component instance can have its own store. If omitted, an empty store is created.render(instance)
: Return an HTML string or a real DOM Element.dependsOn
: Array of keys from your store that trigger re-render. If omitted, any store change re-renders.onMount
/onUnmount
: Optional hooks for lifecycle tasks (e.g. fetching data, clearing timeouts).
Using Components in HTML
In your markup, simply use:
<div data-t-component="my-slider"></div>
That's all you need. If you prefer, you can add more data-*
attributes (like data-theme
, data-initial
, etc.). The plugin automatically collects these attributes in instance.attributes
.
Mounting & Lifecycle
After defining your components, mount them by scanning a DOM subtree for [data-t-component]
:
mountComponents({ root: document.body });
This will:
1. Find all elements with data-t-component="..."
2. Look up the matching definition
3. Create a local store (if applicable) and subscribe to changes
4. Render it for the first time
5. Call onMount(instance)
if provided
When you remove that <div>
from the DOM, a MutationObserver fires, triggering onUnmount(instance)
(if defined) and unsubscribing from the store—preventing memory leaks.
Keys & Cross-Communication
Sometimes you want to manipulate or query a component from outside. That's where data-t-key="..."
helps:
<div data-t-component="my-slider" data-t-key="sliderA"></div>
Later in code:
import { getComponentByKey } from '@tmrw/components';
const sliderA = getComponentByKey('sliderA');
if (sliderA) {
sliderA.store.set('value', 75);
console.log('Slider A attributes:', sliderA.attributes);
}
This is especially handy for inter-component or parent-child communication, letting you control any instance's store or read its attributes directly.
Nested Components
If your render()
function outputs more [data-t-component]
elements, those child components are automatically discovered after each render. For example, "my-modal" might render a "my-button" inside its output. Once the parent is mounted/re-rendered, the plugin recursively calls mountComponents({ root: parentEl })
to handle new children.
You don't have to do anything extra—they're mounted on the fly.
Advanced Patterns
Selective Re-Renders
If your local store has many keys but you only want to re-render on certain ones, setdependsOn: ['foo', 'bar']
.Shared (Global) Store
Instead ofcreateStore()
, you can pass your global store (or skip local store usage entirely). But local stores can keep code simpler if each instance is self-contained.Actions & Effects
You can still register TomorrowJS actions (e.g.,registerAction('myAction', fn)
) and usedata-t-on="event:myAction"
inside your component's output. They'll have direct access to the instance's store.Cleanup
If your component does advanced stuff (like manual event listeners outside the store or network subscriptions), place the teardown inonUnmount(instance)
.
TypeScript Note (safeSubscribe)
In an ideal world, your store's subscribe()
returns a function for unsubscribing (i.e., () => void
). If it doesn't and returns void
, TypeScript can throw "Type 'void' is not assignable to type '() => void'."
To avoid that, this plugin uses an internal safeSubscribe
wrapper:
- If your store properly returns a function, it's used as-is.
- If it returns
void
, we fallback to a no-op—so unsubscribing won't break your app or your types.
This means @tmrw/components
compiles and runs smoothly, even if your store's subscribe()
is partially implemented.
License
This library is released under the MIT License.
@tmrw/components
is designed to make TomorrowJS more powerful while keeping development a breeze. We welcome feedback and contributions—feel free to open an issue or PR on the official repo. Enjoy building your next project with a minimal, HTML-focused approach that doesn't skimp on reusability or developer experience!
Happy coding with TomorrowJS & @tmrw/components
! If you have any issues, suggestions, or just want to say hello, drop by our repo. We'd love to hear from you.