or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/ecos@0.2.x

docs

index.md
tile.json

tessl/npm-ecos

tessl install tessl/npm-ecos@0.2.0

Entity Component System for JavaScript that enables component-based entity creation and management using a factory pattern

working-with-extenders.mddocs/guides/

Working with Extenders

Extenders allow you to customize entity creation by adding property accessors (getters/setters) or running initialization functions. This guide covers how to use extenders effectively in your ECOS applications.

Overview

ECOS provides two types of extenders:

  • GETSET extenders: Add custom getters and setters to entity properties
  • FUNCTION extenders: Execute initialization logic during entity creation

Getting Started with GETSET Extenders

GETSET extenders allow you to create computed properties, validated properties, or properties with side effects.

Step 1: Register a GETSET Extender

const { factory, extenders } = require('ecos');

factory.registerExtender('fullName', {
    type: extenders.GETSET,
    name: 'fullName',
    get: function() {
        return `${this.firstName} ${this.lastName}`;
    },
    set: function(value) {
        const parts = value.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1] || '';
    }
});

Step 2: Apply the Extender to a Factory

const personFactory = factory.create({
    name: 'person',
    props: ['firstName', 'lastName'],
    extend: ['fullName']
});

Step 3: Use the Computed Property

const person = personFactory.create({ firstName: 'John', lastName: 'Doe' });
console.log(person.fullName); // 'John Doe'

person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName);  // 'Smith'

Common GETSET Patterns

Read-Only Properties

Omit the set function to create read-only properties:

factory.registerExtender('experienceToNext', {
    type: extenders.GETSET,
    name: 'experienceToNext',
    get: function() {
        return this.level * 100 - this.experience;
    }
    // No setter - read-only
});

Validated Properties

Use setters to validate and constrain values:

factory.registerExtender('validatedLevel', {
    type: extenders.GETSET,
    name: 'level',
    get: function() {
        return this._level || 1;
    },
    set: function(value) {
        // Clamp level between 1 and 100
        this._level = Math.max(1, Math.min(100, value));
    }
});

const playerFactory = factory.create({
    name: 'player',
    extend: ['validatedLevel']
});

const player = playerFactory.create();
player.level = 200; // Clamped to 100
console.log(player.level); // 100

Properties with Side Effects

Trigger actions when properties change:

factory.registerExtender('temperature', {
    type: extenders.GETSET,
    name: 'celsius',
    get: function() {
        return this._celsius || 0;
    },
    set: function(value) {
        this._celsius = value;
        this._fahrenheit = (value * 9/5) + 32;
    }
});

factory.registerExtender('fahrenheit', {
    type: extenders.GETSET,
    name: 'fahrenheit',
    get: function() {
        return this._fahrenheit || 32;
    }
});

const weatherFactory = factory.create({
    name: 'weather',
    extend: ['temperature', 'fahrenheit']
});

const weather = weatherFactory.create();
weather.celsius = 25;
console.log(weather.fahrenheit); // 77 (automatically calculated)

Getting Started with FUNCTION Extenders

FUNCTION extenders execute custom initialization logic when an entity is created.

Step 1: Register a FUNCTION Extender

const { factory, extenders } = require('ecos');

factory.registerExtender('timestamps', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.createdAt = Date.now();
        entity.updatedAt = Date.now();
    }
});

Step 2: Apply the Extender to a Factory

const entityFactory = factory.create({
    name: 'entity',
    props: ['name'],
    extend: ['timestamps']
});

Step 3: Create Entities with Automatic Initialization

const entity = entityFactory.create({ name: 'MyEntity' });
console.log(entity.createdAt); // Current timestamp
console.log(entity.updatedAt); // Current timestamp

Common FUNCTION Patterns

Generate Unique Identifiers

factory.registerExtender('uuid', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.uuid = Math.random().toString(36).substr(2, 9);
    }
});

Initialize Collections

factory.registerExtender('collections', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.tags = [];
        entity.metadata = {};
    }
});

Complex Initialization with Configuration

Access the extender configuration to customize behavior:

factory.registerExtender('randomize', {
    type: extenders.FUNCTION,
    min: 1,
    max: 100,
    handler: function(entity, extender) {
        const range = extender.max - extender.min;
        entity.randomValue = Math.floor(Math.random() * range) + extender.min;
    }
});

Initialize Nested Objects

factory.registerExtender('initializeGame', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.stats = {
            health: 100,
            mana: 50,
            stamina: 100
        };
        entity.inventory = [];
        entity.equipment = {
            weapon: null,
            armor: null,
            accessory: null
        };
    }
});

Combining GETSET and FUNCTION Extenders

Use both types together for powerful entity initialization:

const { factory, extenders } = require('ecos');

// Initialize internal state with FUNCTION
factory.registerExtender('init', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity._internalValue = 0;
        entity.logs = [];
    }
});

// Add controlled access with GETSET
factory.registerExtender('valueAccessor', {
    type: extenders.GETSET,
    name: 'value',
    get: function() {
        return this._internalValue;
    },
    set: function(val) {
        this.logs.push({
            timestamp: Date.now(),
            oldValue: this._internalValue,
            newValue: val
        });
        this._internalValue = val;
    }
});

const trackedFactory = factory.create({
    name: 'tracked',
    extend: ['init', 'valueAccessor']
});

const entity = trackedFactory.create();
entity.value = 10;
entity.value = 20;
entity.value = 15;

console.log(entity.value); // 15
console.log(entity.logs);  // Array with 3 change records

Understanding Extender Execution

See Execution Order for detailed information about when extenders run during entity creation.

Key point: Extenders execute BEFORE properties are assigned. Properties from default or create(options) are NOT yet available in extender handlers.

Best Practices

Use Descriptive Names

Choose clear, descriptive names for extenders that indicate their purpose:

// Good
factory.registerExtender('validateEmail', {...});
factory.registerExtender('computeFullName', {...});
factory.registerExtender('initializeInventory', {...});

// Avoid
factory.registerExtender('ex1', {...});
factory.registerExtender('helper', {...});

Keep Extenders Focused

Each extender should have a single, clear responsibility:

// Good - separate concerns
factory.registerExtender('timestamps', {...});
factory.registerExtender('uuid', {...});

// Avoid - too many responsibilities
factory.registerExtender('initEverything', {
    type: extenders.FUNCTION,
    handler: function(entity) {
        entity.createdAt = Date.now();
        entity.uuid = Math.random().toString(36).substr(2, 9);
        entity.tags = [];
        // ... many more things
    }
});

Use Methods for Property-Dependent Logic

Since extenders run before properties are assigned, use methods when you need access to default values or create options:

// Register a method for property-dependent initialization
factory.registerMethod('initializeStats', function() {
    this.stats = {
        health: this.baseHealth || 100,
        mana: this.baseMana || 50
    };
});

const factory = factory.create({
    name: 'character',
    props: ['baseHealth', 'baseMana'],
    default: { baseHealth: 150, baseMana: 75 },
    methods: ['initializeStats']
});

const character = factory.create({ name: 'Hero' });
character.initializeStats(); // Call after creation when properties are available

Reuse Extenders Across Factories

Register extenders once and reuse them across multiple factories:

// Register once
factory.registerExtender('timestamps', {...});
factory.registerExtender('uuid', {...});

// Use in multiple factories
const userFactory = factory.create({
    name: 'user',
    extend: ['timestamps', 'uuid']
});

const postFactory = factory.create({
    name: 'post',
    extend: ['timestamps', 'uuid']
});

Error Handling

The factory module throws errors for invalid extender registration:

  • Invalid name: If extender name is not a string
  • Invalid configuration: If extender is not an object
  • Duplicate name: If extender name is already registered
  • Extender not found: If factory references an unregistered extender
try {
    factory.registerExtender('myExtender', {
        type: extenders.FUNCTION,
        handler: function(entity) {
            // ... initialization logic
        }
    });
} catch (error) {
    console.error('Failed to register extender:', error.message);
}

See Also