StringTune/Docs

Custom Modules

Objects and Attributes

How StringObject works, which base properties already exist, and how attribute mapping flows into per-object state.

Objects and Attributes

For element modules, StringObject is the unit of work.

The runtime creates one object per discovered DOM node, stores parsed properties on it, and then reuses that object across modules.

What a StringObject gives you

The exported StringObject surface includes:

  • htmlElement
  • id
  • keys
  • events
  • mirrorObjects
  • connects
  • setProperty(...)
  • getProperty(...)

That is the stable surface you should prefer inside custom modules.

Useful runtime fields already on the object

Some values are also kept as hot-path fields for built-ins:

  • progress
  • progressRaw
  • startPosition
  • differencePosition
  • lerp
  • glide
  • magneticX
  • magneticY

These can be useful when your module intentionally composes with a built-in module, but they are not a replacement for your own module state. For custom authoring, keep your own values under setProperty(...) unless you are deliberately consuming a known built-in contract.

How string becomes object.keys

When ObjectManager adds an element, it reads:

  • string
  • or data-string

Then it splits the value by pipe characters.

So this markup:

HTML
<div string="progress|rotate-progress"></div>

becomes:

TypeScript
object.keys = ['progress', 'rotate-progress'];

That is why one element can connect to several modules at once.

How attribute mapping resolves values

Inside initializeObject(...), the base class resolves each mapped key from:

  • attributes[key]
  • attributes['string-' + key]
  • attributes['data-string-' + key]

Then it falls back to module settings and the mapping fallback.

That means this mapping:

TypeScript
{ key: 'radius', type: 'number', fallback: 150 }

can be configured through:

HTML
<div string-radius="220"></div>

or:

HTML
<div data-string-radius="220"></div>

Fallback functions

Fallbacks do not have to be static.

The base class uses function fallbacks for geometry fields like start, end, and size, and your custom module can do the same:

TypeScript
{
  key: 'my-default',
  type: 'number',
  fallback: (element, object, rect) => rect.width / 2,
}

Use this when the default depends on the element or the current layout.

Object-local events

Each StringObject has its own events emitter.

Built-ins use it for local hooks such as enter and leave:

TypeScript
override onObjectConnected(object: StringObject): void {
  const onEnter = () => object.htmlElement.classList.add('-active');
  const onLeave = () => object.htmlElement.classList.remove('-active');

  object.setProperty('on-enter-handler', onEnter);
  object.setProperty('on-leave-handler', onLeave);

  object.events.on('enter', onEnter);
  object.events.on('leave', onLeave);
}

override onObjectDisconnected(object: StringObject): void {
  object.events.off('enter', object.getProperty('on-enter-handler'));
  object.events.off('leave', object.getProperty('on-leave-handler'));
}

This is the correct pattern for object-local subscriptions.

Mirrored elements

If another element uses string-copy-from="<id>", the source object gets one or more StringMirrorObject instances in object.mirrorObjects.

For most modules you do not need to deal with mirrors manually because StringModule already gives you:

  • applyToElementAndConnects(...)
  • applyVarToConnects(...)
  • applyPropToConnects(...)

Use those helpers so your module respects mirrors without duplicating logic.

Practical rule

Treat StringObject as the shared state boundary between modules.

That usually means:

  • parse into object.setProperty(...)
  • compute from those properties
  • write output through the module helpers

Do not mutate unrelated built-in hot fields unless your module is intentionally part of that exact contract.