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
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
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
<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:
StringProgressis registered- the element connects to
string="progress" object.progressis 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 onprogressexplicit.onMutate(...)owns all writes.object.progressis 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.