Home Doh Ref
Dohballs
  • 📁 doh_modules
    • 📦 dataforge
    • 📦 express
    • 📁 sso
    • 📁 user

The html Pattern: DOM and UI Components

HTML

The html Pattern provides an object-oriented interface for creating and managing DOM elements within Doh applications. It handles DOM manipulation, event handling, and the creation of reusable UI components.

This guide covers:

  • Basic HTML element creation and manipulation
  • Pattern-level CSS styling
  • Using property proxies (classes, css, attr)
  • Event handling and the html_phase lifecycle phase
  • Integration with the Sub-Object-Builder for nested elements
  • Data binding with HTML elements

jQuery Dependency: Note that the html pattern relies heavily on jQuery for its core DOM manipulation and event handling capabilities, accessed primarily through the this.e property (which is a jQuery object wrapping the element).

Basic Usage

Create an HTML object instance using the New function:

let myDiv = New('html', {
    //tag: 'div',  // div is the default tag
    css: { backgroundColor: 'lightblue', padding: '10px' },
    html: 'Hello, Doh!', // Sets innerHTML
    attr: { id: 'myUniqueDiv' },
    // This button will be auto-built via the Sub-Object-Builder
    button: {
        pattern: 'html', 
        tag: 'button',
        html: 'Click me'
    }
});

// Default Behavior: Automatic DOM Append
// By default, the created element (myDiv.e) is automatically appended 
// to document.body during its 'html_phase'. This can be customized.

This creates a <div> element (the default tag if unspecified), applies styles and attributes, sets its content, and uses the Sub-Object-Builder to create a nested button.

Auto-Append Behavior: By default, HTML elements are automatically appended to their .builder(or .parent) property (or document.body if no parent is specified) during the html_phase.

Pattern-level CSS Styling

Doh provides a way to define CSS styles at the pattern level, which are then applied to all instances of that pattern. This approach offers:

  1. Consistent styling across all instances of a pattern
  2. CSS classes instead of inline styles for performance
  3. Cleaner component code without repetitive style definitions

When you define a pattern with css or style properties, Doh automatically:

Pattern('StyledCard', 'html', {
    // These CSS properties will be converted to a class
    css: {
        backgroundColor: 'white',
        borderRadius: 8,        // Numeric values are automatically converted to 'px'
        padding: 20,            // This becomes '20px' in the CSS
        boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
        margin: '10px 0'
    },
    // Style string is also supported and merged with css object
    style: 'display: flex; flex-direction: column;'
});

// Create an instance - it will automatically have the pattern's styling
let card = New('StyledCard', {
    html: 'This card has pre-defined styling from its pattern'
});

How Pattern Styling Works

Behind the scenes, when you define a pattern with CSS:

  1. A unique CSS class name is generated (e.g., 'doh-StyledCard')
  2. A stylesheet rule is created with all the defined styles
  3. The class is added to the pattern's classes array
  4. When instances are created, they automatically receive this class
  5. Numeric values are automatically converted to pixels (except for z-index and opacity)
  6. The original CSS and style definitions are stored in initial_css and initial_style for reference

This system lets you separate style definitions from component behavior.

Property Proxies

The html pattern provides proxies for interacting with common element properties.

Classes Proxy

let myElement = New('html', {
    tag: 'span',
    html_phase: function() {
        // Add classes
        this.classes.push('highlight');
        this.classes('important', 'large');

        // Remove a class
        delete this.classes.important;

        // Check if a class exists
        if ('highlight' in this.classes) {
            console.log('Element is highlighted');
        }

        // Iterate over classes
        for (let className of this.classes) {
            console.log(className);
        }

        // Get class count
        console.log(this.classes.length);

        // Convert to string
        console.log(this.classes.toString());
    }
});

CSS Proxy

let styledElement = New('html', {
    tag: 'p',
    html_phase: function() {
        // Set individual properties
        this.css.color = 'navy';
        this.css.fontSize = '16px';

        // Get a property
        console.log(this.css.color);

        // Remove a property
        this.css.color = '';

        // Batch update
        this.css({
            backgroundColor: 'lightyellow',
            border: '1px solid gray',
            padding: '5px'
        });

        // Vendor prefixes
        this.css.webkitTransform = 'rotate(45deg)';

        // Get computed style (read-only)
        console.log(this.css.display);
    }
});

Attributes Proxy

let attributeElement = New('html', {
    tag: 'a',
    html_phase: function() {
        // Set attributes
        this.attr.href = 'https://example.com';
        this.attr.target = '_blank';

        // Get an attribute
        console.log(this.attr.href);

        // Remove an attribute
        delete this.attr.target;

        // Check attribute existence
        if ('href' in this.attr) {
            console.log('Link has an href');
        }

        // Batch update
        this.attr({
            'data-custom': 'value',
            'aria-label': 'Visit Example'
        });

        // Boolean attributes
        this.attr.disabled = true;
        console.log(this.attr.disabled); // Returns 'disabled' if set, undefined otherwise
    }
});

Event Handling: The .on Collection System

The html pattern provides a sophisticated event handling system through the .on collection that automatically binds event handlers during the on_phase lifecycle phase.

The .on Passthrough Collection

The most powerful way to handle events is through the .on property, which uses Doh's advanced Method Melding system with method hoisting for natural event composition:

let smartButton = New('html', {
    tag: 'button',
    html: 'Smart Click Me',
    
    // Event handlers in the 'on' collection - 'this' is hoisted to the HTML element
    on: {
        click: function(event) {
            console.log('Button clicked!');
            this.css.backgroundColor = 'yellow'; // 'this' refers to HTML element, not .on object
            this.clickCount = (this.clickCount || 0) + 1; // Direct property access
        },
        
        mouseenter: function(event) {
            this.css.opacity = '0.8'; // Natural access to element properties
        },
        
        mouseleave: function(event) {
            this.css.opacity = '1';
        },
        
        focus: function(event) {
            this.css.borderColor = 'blue';
        },
        
        blur: function(event) {
            this.css.borderColor = '';
        }
    }
});

Method Hoisting in Nested MOC Properties

The key insight is method hoisting: even though handlers are defined in this.on.click, the this context inside the handler is hoisted to refer to the outer HTML element object. This is controlled by the nested MOC definition:

// From html pattern MOC:
moc: {
  on: {'*': 'async_method'}  // All properties in .on use async_method melding with hoisting by default
}

This means:

  • this.on.click() executes with this referring to the HTML element
  • Event Handlers can be async and will be awaited in order of execution
  • Event handlers naturally access this.css, this.attr, this.e, etc.
  • The .on collection feels integrated, not like a separate object

Method Melding for Event Handlers

The .on collection uses Method melding (MOC type 'method') by default, enabling sophisticated event composition across pattern inheritance:

Pattern('ClickableBase', 'html', {
    on: {
        click: async function(event) {
            console.log('Base click behavior');
            // Only return values when you need to compose state
            return { clickCount: (this.clickCount || 0) + 1 }; // This gets blended onto the HTML element
        }
    }
});

Pattern('ExtendedClickable', 'ClickableBase', {
    on: {
        click: function(event) {
            // Base handler runs first due to Method melding (Pattern Resolution Order)
            console.log('Extended click behavior - count:', this.clickCount);
            // No return needed unless composing additional state
        }
    }
});

Custom Event Handler Melding

You can customize how individual event handlers compose by overriding their MOC type in your pattern:

Pattern('CustomEventMelding', 'html', {
    moc: {
        on: {
            // Override specific event handlers with different melding strategies  
            click: 'funnel',      // Reverse order: derived→base
            mousedown: 'chain',   // Pipeline: return value → next handler  
            keypress: 'override'  // No melding, replace completely
        }
    },
    
    on: {
        click: function(event) {
            console.log('Base click (runs LAST due to funnel)');
            this.css.backgroundColor = 'blue';
        },
        
        mousedown: function(event) {
            console.log('Base mousedown');
            return 'processed';  // Passed to next handler in chain
        }
    }
});

Pattern('ExtendedCustom', 'CustomEventMelding', {
    on: {
        click: function(event) {
            console.log('Extended click (runs FIRST due to funnel)');
            this.css.color = 'white';
        },
        
        mousedown: function(processedValue, event) {
            // processedValue = 'processed' from base handler
            console.log('Extended mousedown received:', processedValue);  
            this.attr.title = `Processed: ${processedValue}`;
        }
    }
});

Event Lifecycle Phases

The html pattern implements a sophisticated phase system where on_phase runs BEFORE html_phase:

  1. object_phase - DOM element created (this.e becomes available)
  2. on_phase - Event handlers from .on collection automatically bound via update_on()
  3. html_phase - Element appended to DOM, additional DOM setup occurs

This ensures events work from first birth - handlers are ready before the element enters the document.

let phasedElement = New('html', {
    tag: 'div',
    
    on: {
        click: function() { 
            console.log('Auto-bound during on_phase'); 
            this.css.backgroundColor = 'green';
        }
    },
    
    html_phase: function() {
        // Events from .on collection are already bound by this point
        console.log('Element now in DOM with events ready');
        
        // You can still bind additional events manually here
        this.e.on('dblclick', () => {
            console.log('Manually bound during html_phase');
            this.css.border = '2px solid red';
        });
    }
});

Custom Event Setup and Handler Reuse

For custom event setup, handler reuse, or dynamic event management, use the appropriate lifecycle phases:

let advancedElement = New('html', {
    tag: 'button',
    
    // Pre-phase setup for reusing handlers or custom configuration
    pre_on_phase: function() {
        // Create shared handler for reuse
        this.borderToggle = function(event) {
            this.css.borderColor = event.type === 'focus' ? 'blue' : '';
        };
        
        // Set up handlers that will be bound during on_phase
        this.on.focus = this.borderToggle;
        this.on.blur = this.borderToggle;
    },
    
    on: {
        mouseover: function() { 
            console.log('Mouse over!');
            this.css.opacity = '0.8'; 
        }
    },
});

//For complete control or when you need features not available through the `.on` collection, you can still bind events directly in `on_phase`:
let directBinding = New('html', {
    tag: 'button',
    html: 'Direct Binding',
    
    html_phase: function() { 
        // Direct jQuery binding (not managed by .on collection)
        this.e.on('click', () => {
            console.log('Directly bound event');
            this.css.backgroundColor = 'yellow';
        });

        // Multiple events
        this.e.on('mouseenter mouseleave', (event) => {
            this.css.opacity = event.type === 'mouseenter' ? '0.8' : '1';
        });

        // Delegated events (for child elements)
        this.e.on('click', '.child-element', (event) => {
            console.log('Child element clicked');
        });
    }
});

Event Management Methods (Melded)

The event management methods (update_on, pause_on, resume_on) are melded methods themselves, meaning you can hook into them:

let managedElement = New('html', {
    tag: 'button',
    on: {
        click: function() { 
            console.log('Clicked!'); 
            this.css.backgroundColor = 'yellow';
        }
    },
    
    // Hook into the melded update_on method
    pre_update_on: function(desired_props) {
        console.log('About to update event handlers:', desired_props);
    },
    
    post_pause_on: function(desired_props) {
        console.log('Paused event handlers:', desired_props);
        this.css.opacity = '0.5'; // Visual indication of paused state
    },
    
    html_phase: function() {
        // These methods are melded, so they support the full inheritance chain
        this.pause_on('click'); // Pause specific events
        this.pause_on(['click', 'mouseover']); // Array of events
        
        this.resume_on('click'); // Resume specific events  
        this.resume_on(['click', 'mouseover']);
        
        this.update_on(); // All handlers in .on collection
        this.update_on('click'); // Just click handler
        this.update_on(['click', 'mouseover']); // Specific handlers
    }
});

Lifecycle Phases

The html pattern inherits the standard object lifecycle phases (object_phase, builder_phase, etc.) and adds the html_phase. You can use hooks (pre_, post_) for any phase.

Note: When using New(), html_phase and its pre_/post_ hooks must be synchronous. If you need asynchronous phases, use AsyncNew() instead, which works the same way but allows async phase handlers and returns a Promise that resolves to the object. Within html_phase (and its pre_/post_ hooks), you can access this.e.

Pattern('CustomHtmlComponent', 'html', {
    //tag: 'div',
    object_phase: function() {
        // Runs early: Basic setup, data initialization
        this.data = { clicks: 0 };
    },
    
    builder_phase: function() {
        // Runs after object_phase: Child elements are built here
        // (e.g., if this pattern had properties with a 'pattern' key)
    },

    pre_html_phase: function(){
        // Runs just before element is appended to DOM
        // Last-minute setup before rendering
    },

    html_phase: function() {
        // Runs after element is in the DOM
        // Event binding, post-append manipulation
        this.e.on('click', () => {
            this.data.clicks++;
            this.childButton.html = `Clicks: ${this.data.clicks}`;
        });
    },

    post_html_phase: function(){
        // Runs after html_phase completes
    },

    // Define a child to be built automatically
    childButton: { pattern: 'html', tag: 'button', html: 'Clicks: 0' }
});

New('CustomHtmlComponent');

DOM Manipulation (via jQuery Proxy this.e)

Direct DOM manipulation relies on the this.e property, which is a jQuery object wrapping the component's root DOM element.

let parentElement = New('html', {
    html_phase: function() {
        // Use standard jQuery methods on this.e
        this.e.text('New Text Content');
        this.e.addClass('processed');
        this.e.append('<p>Appended paragraph</p>');
    }
});

Control Registration System (Simplified)

Doh provides a mechanism for UI components (often inheriting html or control patterns) to automatically register themselves with a designated parent "controller" object. This happens during the component's control_phase (another synchronous lifecycle phase).

  • The component finds its controller (usually an ancestor with is_controller: true).
  • It registers itself in the controller's controls object (e.g., controller.controls.myButton = this).

This lets controllers manage their child components. Registration for organization.

Data Binding

You can use Doh's data binding tools (Doh.mimic, Doh.observe) with html pattern instances.

You can observe nested properties within the css and attr proxies. This lets you react to changes in specific styles or attributes applied to the DOM element, even if they are changed in the DOM outside of Doh.

let myElement = New('html', { css: { color: 'blue' } });

// Observe changes to the element's color style
Doh.observe(myElement.css, 'color', (target, prop, newValue, oldValue) => {
    console.log(`Color changed from ${oldValue} to ${newValue}`);
});

// Later, changing the proxy property triggers the observer AND updates the DOM
myElement.css.color = 'red'; // Logs: "Color changed from blue to red"

Immediate reads after writes

  • Updates via observe/mimic propagate on the microtask queue. If you set a value and then immediately read a mirrored/derived value in the same tick (e.g., testing attr/css sync), flush first:
await Doh.microtask();

See /docs/core/data-binding#timing-and-microtasks.

Last updated: 10/22/2025