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:
htmlElementidkeyseventsmirrorObjectsconnectssetProperty(...)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:
progressprogressRawstartPositiondifferencePositionlerpglidemagneticXmagneticY
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:
<div string="progress|rotate-progress"></div>
becomes:
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:
{ key: 'radius', type: 'number', fallback: 150 }
can be configured through:
<div string-radius="220"></div>
or:
<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:
{
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:
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.