0.0.4 • Published 6 months ago

@tmrw/component v0.0.4

Weekly downloads
-
License
-
Repository
-
Last release
6 months ago

@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
    Use data-t-key="someKey" so you can grab a component anywhere with getComponentByKey("someKey")—ideal for dashboards, widgets, or advanced flows.

  • Safe & Type-Friendly
    Includes a safeSubscribe wrapper to avoid TypeScript errors if your store doesn't fully conform to the ideal () => void unsubscribe signature.

Table of Contents

  1. Installation
  2. Basic Example
  3. Defining Components
  4. Using Components in HTML
  5. Mounting & Lifecycle
  6. Keys & Cross-Communication
  7. Nested Components
  8. Advanced Patterns
  9. TypeScript Note
  10. 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 in data-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

  1. Selective Re-Renders
    If your local store has many keys but you only want to re-render on certain ones, set dependsOn: ['foo', 'bar'].

  2. Shared (Global) Store
    Instead of createStore(), 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.

  3. Actions & Effects
    You can still register TomorrowJS actions (e.g., registerAction('myAction', fn)) and use data-t-on="event:myAction" inside your component's output. They'll have direct access to the instance's store.

  4. Cleanup
    If your component does advanced stuff (like manual event listeners outside the store or network subscriptions), place the teardown in onUnmount(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.

0.0.4

6 months ago

0.0.3

6 months ago

0.0.2

6 months ago

0.0.1

7 months ago