The Sub-Object-Builder is enabled by the base object
pattern, that helps you create hierarchies of nested objects in Doh using simple property definitions. It automatically builds child object instances from your declarations, making it easier to create and manage complex component structures without manual instantiation.
This guide covers:
Understanding Doh's object instantiation lifecycle is essential to effectively using the sub-object-builder system.
When you create a Doh object using New()
, the object goes through several phases:
object_phase
: Basic object setup and initialization.builder_phase
: This is where the Sub-Object-Builder system runs, automatically constructing child objects.Crucially, properties intended for auto-building must be defined before the builder_phase
executes.
The recommended approach is to define buildable properties directly in the idea
object literal passed to New()
or within the Pattern
definition itself.
// PREFERRED: Define properties in the idea object literal
let myObject = New('my_pattern', {
// Direct properties that will be auto-built
header: {
pattern: 'html', // The 'pattern' key signals auto-building
tag: 'header'
},
content: {
pattern: 'html',
tag: 'main'
}
});
// GOOD: Define properties in the pattern definition
Pattern('my_pattern', {
header: {
pattern: 'html',
tag: 'header'
},
footer: {
pattern: 'html',
tag: 'footer'
}
});
Using the object_phase
to define buildable properties should be reserved for cases where the structure is dynamic and cannot be determined beforehand.
Pattern('my_pattern', {
object_phase: function() {
// LESS PREFERRED: Only use for dynamic properties
if (this.config.dynamicLayout) {
this.dynamicSection = { pattern: 'html', tag: 'section' };
}
}
});
The builder_phase
(provided by the base object
pattern) then finds and processes these definitions:
// The standard builder_phase logic
builder_phase: function() {
// 1. Collect properties containing a 'pattern' key
Doh.collect_buildable_ideas(this, this.moc, this, '');
// 2. Build child objects using New() if any were found
if (this.built) {
this.machine_built(this.machine_built_to); // Instantiates items in this.built
}
}
Remember the lifecycle phase execution order:
object_phase
builder_phase
Each phase supports pre_
and post_
hooks, allowing fine-grained control. See Method Hooks below.
A property is identified as buildable and processed by the Sub-Object-Builder if:
pattern
whose value is a string (the name of the Pattern to instantiate).// This property will be auto-built
header: {
pattern: 'html', // <<< Required key
tag: 'header'
}
// This will NOT be auto-built (no 'pattern' key)
footer: {
tag: 'footer'
}
Doh strongly encourages using named, direct properties for defining child objects, as this is what the Sub-Object-Builder primarily looks for.
// PREFERRED: Direct properties
Pattern('GoodPattern', {
header: { pattern: 'html', tag: 'header' },
navigation: { pattern: 'html', tag: 'nav' },
content: { pattern: 'html', tag: 'main' }
});
While Doh does provide a children
array for object relationships, direct properties are favored for auto-building.
During the builder_phase
, the system iterates through properties identified as buildable:
New()
using the property's value (the idea
object containing the pattern
key and other properties) as the idea
argument and passes down a target phase. The target phase is determined by the parent's machine_built_to
property (or defaults to 'final'
). See Coordinating Child Lifecycles below.idea
used to construct the child is preserved internally on the child at childInstance.inherited.idea
.builder
property is added to the child, linking it to the parent.// Before builder_phase runs on parent
myObject.header = { pattern: 'html', tag: 'header', machine_built_to: 'object_phase' };
myObject.content = { pattern: 'html', tag: 'main' }; // Will build to 'final' (default)
// After builder_phase runs on parent
myObject.header = /* Instance of 'html', machined only to 'object_phase' */;
myObject.header.builder = myObject;
myObject.content = /* Instance of 'html', machined to 'final' */;
myObject.content.builder = myObject;
The Sub-Object-Builder handles nested definitions recursively. When a child object is built, its own builder_phase
runs, potentially building its children.
// Define a complex nested structure
let page = New('object', {
body: { // Level 1
pattern: 'html',
tag: 'body',
header: { // Level 2
pattern: 'html',
tag: 'header',
title: { // Level 3
pattern: 'html',
tag: 'h1',
html: 'Welcome'
}
},
content: { // Level 2
pattern: 'html',
tag: 'main'
}
}
});
// After building, you have a complete hierarchy:
// page.body
// page.body.header
// page.body.header.title
// page.body.content
The Melded Object Composition (MOC) system provides hooks that are useful in the context of sub-object-building.
pre_
and post_
Hooks for PhasesYou can define pre_builder_phase
and post_builder_phase
methods (and hooks for any phase) to run code immediately before or after the main phase logic.
pre_builder_phase
: The ideal place to dynamically define properties that need to be auto-built. This hook runs before the builder collects buildable properties.post_builder_phase
: Runs after all child objects for the current object have been built. Useful for setting up relationships between siblings or between parent and children.Pattern('PhasedBuilderPattern', {
moc: {
object_phase: 'phase',
builder_phase: 'phase'
// custom_phase: 'phase' // example
},
// Define dynamic children just before building starts
pre_builder_phase: function() {
if (this.config.needsWidget) {
this.widget = { pattern: 'Widget', config: this.config.widgetConf };
}
},
// Setup relationships after children are built
post_builder_phase: function() {
if (this.widget && this.mainContent) {
// Example: Let widget know about main content area
this.widget.contentArea = this.mainContent;
console.log('Widget and Main Content linked.');
}
}
});
As described in the MOC documentation, when inheriting multiple patterns, hooks execute in a specific order relative to the main phase methods:
pre_
hooks (inheritance order: base to derived)post_
hooks (inheritance order: base to derived)Auto-built objects automatically establish parent-child relationships:
builder
Property: Each auto-built child object receives a builder
property that points directly to its parent (the object it was defined within).this.builder_property('propertyName')
: Searches up the builder chain for the first ancestor with the property.this.builder_method('methodName')
: Searches up the builder chain for the first ancestor with the method and returns a bound function.Pattern('ParentPattern', {
parentData: 'Some Value',
parentMethod: function() { console.log('Parent method called!'); },
// Child property that will be auto-built
child: {
pattern: 'ChildPattern'
}
});
Pattern('ChildPattern', {
accessParent: function() {
// Direct access via builder property
console.log(this.builder.parentData); // -> Some Value
}
});
const instance = New('ParentPattern');
instance.child.accessParent();
It is possible to influence the lifecycle phases child objects are initialized to during the automatic build process using the parent object's machine_built_to
property. This is an advanced mechanism primarily used to coordinate the initialization of UI controls after they have registered but before they are fully rendered.
For a detailed explanation of this mechanism and its primary use case within the control registration system, please see the Control Registration System documentation in the html pattern guide.
Define buildable properties directly in the idea
object (passed to New()
) or within the Pattern
definition, rather than dynamically in phases, unless necessary.
// BEST: In idea
let obj = New('object', { child: { pattern: 'X' } });
// GOOD: In pattern
Pattern('Parent', { child: { pattern: 'X' } });
// OK (Only if dynamic): In pre_builder_phase
Pattern('DynamicParent', {
pre_builder_phase: function() { this.child = { pattern: 'X' }; }
});
// BAD: In builder_phase or later (too late for auto-build)
Pattern('LateParent', {
builder_phase: function() { /* ... */ this.child = { pattern: 'X' }; }
});
Use named, direct properties for children. This leverages the Sub-Object-Builder directly and makes the structure clearer.
// PREFERRED
Pattern('Layout', {
header: { pattern: 'Header' },
footer: { pattern: 'Footer' }
});
// DISCOURAGED for auto-building
Pattern('Layout', {
object_phase: function() {
this.children = [
{ pattern: 'Header' },
{ pattern: 'Footer' }
];
}
});
pre_builder_phase
to define dynamic child properties.post_builder_phase
to configure relationships between children or between parent and children after they are built.While the system supports deep nesting, overly nested structures can become difficult to manage. Consider flattening hierarchies where appropriate by composing smaller, focused patterns.
pattern
key during the builder_phase
.pre_builder_phase
for dynamic definitions.builder
, builder_property
, builder_method
).post_builder_phase
for setup that requires children to exist.