StringTune/Docs

Custom Modules

Worked Example

End-to-end example of an element module that composes with StringProgress and publishes its own output channel.

Worked Example

This example builds a custom module that depends on StringProgress.

It reads scroll progress from the shared StringObject, rotates the element, writes a custom CSS variable, mirrors that output to string-copy-from targets, and emits its own object-scoped event.

Module code

TypeScript
import {
  StringContext,
  StringModule,
  StringObject,
} from '@fiddle-digital/string-tune';

export class StringRotateProgress extends StringModule {
  constructor(context: StringContext) {
    super(context);
    this.htmlKey = 'rotate-progress';

    this.cssProperties = [
      { name: '--rotate-progress', syntax: '<number>', initialValue: '0', inherits: true },
    ];

    this.attributesToMap = [
      ...this.attributesToMap,
      { key: 'rotate', type: 'number', fallback: 180 },
    ];
  }

  override canConnect(object: StringObject): boolean {
    return object.keys.includes('progress') && object.keys.includes('rotate-progress');
  }

  override onMutate(): void {
    for (let i = 0; i < this.objects.length; i++) {
      const object = this.objects[i];
      const progress = object.progress ?? 0;
      const amount = object.getProperty<number>('rotate') ?? 180;
      const rotation = progress * amount;
      const prevRotation = object.getProperty<number>('rotate-applied');

      if (prevRotation === rotation) {
        continue;
      }

      object.setProperty('rotate-applied', rotation);

      this.applyToElementAndConnects(object, (el) => {
        this.tools.styleTxn.setVar(el, '--rotate-progress', progress);
        this.tools.styleTxn.setProp(el, 'transform', `rotate(${rotation}deg)`);
      });

      this.events.emit(
        this.getObjectEventName(object, 'object:rotate-progress'),
        { progress, rotation },
      );
    }
  }

  override onObjectDisconnected(object: StringObject): void {
    const clear = (el: HTMLElement) => {
      el.style.removeProperty('--rotate-progress');
      el.style.removeProperty('transform');
    };

    clear(object.htmlElement);
    for (const mirror of object.mirrorObjects) {
      clear(mirror.htmlElement);
    }
  }
}

Registration

TypeScript
import StringTune, {
  StringProgress,
} from '@fiddle-digital/string-tune';
import { StringRotateProgress } from './modules/StringRotateProgress';

const stringTune = StringTune.getInstance();

stringTune.use(StringProgress);
stringTune.use(StringRotateProgress, {
  rotate: 240,
});

stringTune.start(60);

Markup

HTML
<section
  string="progress|rotate-progress"
  string-id="hero-rotate"
  string-rotate="270"
>
  Rotate me
</section>

<div string-copy-from="hero-rotate"></div>

What this module is intentionally depending on

This module composes with the built-in progress contract.

That means it assumes:

  • StringProgress is registered
  • the element connects to string="progress"
  • object.progress is updated by the progress module before mutate output runs

That is a valid custom-module dependency because it is explicit and local. It would be a bad design only if the module silently depended on private state that no one reading the markup could infer.

Why this example is structured this way

  • canConnect(...) makes the dependency on progress explicit.
  • onMutate(...) owns all writes.
  • object.progress is consumed as shared state instead of re-implementing progress math.
  • applyToElementAndConnects(...) keeps mirrors in sync automatically.
  • getObjectEventName(...) publishes a stable object-scoped event.
  • onObjectDisconnected(...) only removes properties owned by this module.

When to use this pattern

Use this pattern when you want a custom module to:

  • build on top of a built-in module
  • publish a project-specific output channel
  • keep the HTML contract declarative
  • avoid duplicating runtime math that StringTune already computes

That is one of the strongest reasons to write custom modules in StringTune instead of bolting standalone code onto the page.