html
Pattern: DOM and UI ComponentsThe 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:
classes
, css
, attr
)html_phase
lifecycle phasejQuery Dependency: Note that the
html
pattern relies heavily on jQuery for its core DOM manipulation and event handling capabilities, accessed primarily through thethis.e
property (which is a jQuery object wrapping the element).
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
.
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:
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'
});
Behind the scenes, when you define a pattern with CSS:
classes
arrayinitial_css
and initial_style
for referenceThis system lets you separate style definitions from component behavior.
The html
pattern provides proxies for interacting with common element properties.
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());
}
});
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);
}
});
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
}
});
.on
Collection SystemThe html
pattern provides a sophisticated event handling system through the .on
collection that automatically binds event handlers during the on_phase
lifecycle phase.
.on
Passthrough CollectionThe 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 = '';
}
}
});
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 elementthis.css
, this.attr
, this.e
, etc..on
collection feels integrated, not like a separate objectThe .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
}
}
});
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}`;
}
}
});
The html
pattern implements a sophisticated phase system where on_phase
runs BEFORE html_phase
:
object_phase
- DOM element created (this.e
becomes available)on_phase
- Event handlers from .on
collection automatically bound via update_on()
html_phase
- Element appended to DOM, additional DOM setup occursThis 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';
});
}
});
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');
});
}
});
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
}
});
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');
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>');
}
});
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).
is_controller: true
).controls
object (e.g., controller.controls.myButton = this
).This lets controllers manage their child components. Registration for organization.
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
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.