Option name | Type | Description |
---|---|---|
scope | Scope | scope to which component will belong. It is usually a top level scope object returned by |
element | Element | DOM element that component is attached to |
name | String | component name, should be unique in the scope of component |
componentInfo | ComponentInfo | instance of ComponentInfo class that can be used to create a copy of component TODO try removing it |
return | Component |
milo.Component
Base Component class. Subclass of FacetedObject, but none of this class methods should be directly used with component.
Its constructor passes its parameters, including its scope, DOM element and name to init
method.
The constructor of Component class rarely needs to be used directly, as milo.binder creates components when it scans DOM tree.Component.createComponentClass
should be used to create a subclass of Component class with configured facets.
####Component instance properties####
___milo_component
property of element (property name be changed using milo.config
).component.scope._hostObject.owner
, where _hostObject
refers to Container facet of parent component and owner
to the parent itself. The children of component are accessible via the scope of its container facet: component.container.scope
. The scope hierarchy can be the same or different as the DOM hierarchy - DOM children of the component will be on the same scope as component if it does not have Container
facet and in the scope of its Container facet if it has it. See Scope.####Component events####
milo.binder
(as is almost always the case, as all Component class methods that create/copy components use milo.binder
internally - component constructor and Component.create methods are not used in framework outside of milo.binder
and rarely if ever need to be used in aplication).createFromDataTransfer
) method. Can be dispatched by application if the component's state is set with some other mechanism. This event is not used in milo
, it can be used in application in particular subclasses of component.####Component "lifecycle"####
init
method, but most components do not need to do it.init
methods of all facets are called in sequence. Same as components, facet do not implement their constructors, they can optionally implement init
and start
methods (see below). Inside init
method there should be only general initialization code without any dependency on component itself (it is not ready yet) and other facets (as there is no specific facets creation order). If facet implements init
method it MUST call inherited init with ComponentFacet.prototype.init.apply(this, arguments)
.init
method of component is called. At this point all facets are created but facets still can be not ready as they can have initialization code in start
method. If component subclass implements init
method it MUST call inherited method with <Superclass>.prototype.init.apply(this, arguments)
, where check
method of all facets is called. This method adds facets that are not part of the component declaration (being part of the class or explicitely listed in bind attribute) but are required by facets that the compnent already has. Subclasses of ComponentFacet do not need to implement this method.start
method of all facets is called. This method is usually implemented by ComponentFacet subclasses and it can have any initialization code that depends on component or on other facets that are the dependencies of a facet. Inherited start
method should be called int he same way as written above.start
method of component is called. This component method can be implemented by subclasses if they need to have some initialization code that depends on some facets and requires that these facets are fully inialized. Often such code also depends on component's scope children as well so this code should be inside 'childrenbound'
event subscriber.milo.binder
.var Component = _.createSubclass(FacetedObject, 'Component', true);
module.exports = Component;
_registerWithDomStorage('Component');
####Component class methods####
_.extend(Component, {
createComponentClass: Component$$createComponentClass,
create: Component$$create,
copy: Component$$copy,
createOnElement: Component$$createOnElement,
isComponent: componentUtils.isComponent,
getComponent: componentUtils.getComponent,
getContainingComponent: componentUtils.getContainingComponent,
createFromState: Component$$createFromState,
createFromDataTransfer: Component$$createFromDataTransfer
});
delete Component.createFacetedClass;
####Component instance methods####
#####Messenger methods available on component#####
_.extendProto(Component, {
init: Component$init,
start: Component$start,
createElement: Component$createElement,
hasFacet: Component$hasFacet,
addFacet: Component$addFacet,
allFacets: Component$allFacets,
rename: Component$rename,
remove: Component$remove,
insertInto: Component$insertInto,
getState: Component$getState,
getTransferState: Component$getTransferState,
_getState: Component$_getState,
setState: Component$setState,
getScopeParent: Component$getScopeParent,
getTopScopeParent: Component$getTopScopeParent,
getScopeParentWithClass: Component$getScopeParentWithClass,
getTopScopeParentWithClass: Component$getTopScopeParentWithClass,
setScopeParentFromDOM: Component$setScopeParentFromDOM,
walkScopeTree: Component$walkScopeTree,
treePathOf: Component$treePathOf,
getComponentAtTreePath: Component$getComponentAtTreePath,
insertAtTreePath: Component$insertAtTreePath,
broadcast: Component$broadcast,
destroy: Component$destroy,
isDestroyed: Component$isDestroyed
});
Expose Messenger methods on Component prototype
var MESSENGER_PROPERTY = '_messenger';
Messenger.useWith(Component, MESSENGER_PROPERTY, Messenger.defaultMethods);
var COMPONENT_DATA_TYPE_PREFIX = 'x-application/milo-component';
var COMPONENT_DATA_TYPE_REGEX = /x-application\/milo-component\/([a-z_$][0-9a-z_$]*)(?:\/())/i;
Option name | Type | Description |
---|---|---|
name | String | class name |
facetsConfig | Object.<Object>, Array.<String> | map of facets configuration. If some facet does not require configuration, |
return | Subclass.<Component> |
Component class method
Creates a subclass of component from the map of configured facets.
This method wraps and replaces createFacetedClass
class method of FacetedObject.
Unlike createFacetedClass, this method take facet classes from registry by their name, so only map of facets configuration needs to be passed. All facets classes should be subclasses of ComponentFacet
function Component$$createComponentClass(name, facetsConfig) {
// convert array of facet names to map of empty facets configurations
if (Array.isArray(facetsConfig)) {
var configMap = {};
facetsConfig.forEach(function(fct) {
var fctName = _.firstLowerCase(fct);
configMap[fctName] = {};
});
facetsConfig = configMap;
}
// construct map of facets classes from facetRegistry
var facetsClasses;
if (typeof facetsConfig == 'object' && _.keys(facetsConfig).length) {
facetsClasses = {};
_.eachKey(facetsConfig, function(fctConfig, fct) {
var fctName = _.firstLowerCase(fct);
var fctClassName = _.firstUpperCase(fct);
facetsClasses[fctName] = facetsRegistry.get(fctClassName);
});
}
// create subclass of Component using method of FacetedObject
var ComponentClass = FacetedObject.createFacetedClass.call(this, name, facetsClasses, facetsConfig);
_registerWithDomStorage(name);
return ComponentClass;
}
function _registerWithDomStorage(className) {
DOMStorage.registerDataType(className, Component_domStorageSerializer, Component_domStorageParser);
}
function Component_domStorageSerializer(component) {
var state = component.getState();
return JSON.stringify(state);
}
function Component_domStorageParser(compStr, compClassName) {
var state = _.jsonParse(compStr);
if (state)
return Component.createFromState(state);
}
Option name | Type | Description |
---|---|---|
info | ComponentInfo | |
throwOnErrors | Boolean | If set to false, then errors will only be logged to console. True by default. |
return | Component |
Component class method
Creates component from ComponentInfo (used by milo.binder and to copy component)
Component of any registered class (see componentsRegistry) with any additional registered facets (see facetsRegistry) can be created using this method.
function Component$$create(info, throwOnErrors) {
var ComponentClass = info.ComponentClass;
if (typeof ComponentClass != 'function') {
var message = 'create: component class should be function, "' + typeof ComponentClass + '" passed';
if (throwOnErrors === false) {
logger.error('Component', message, ';using base Component class instead');
ComponentClass = Component;
} else
throw new Error(message);
}
var aComponent = new ComponentClass(info.scope, info.el, info.name, info);
if (info.extraFacetsClasses)
_.eachKey(info.extraFacetsClasses, function(FacetClass) {
if (! aComponent.hasFacet(FacetClass))
aComponent.addFacet(FacetClass, undefined, undefined, throwOnErrors);
});
return aComponent;
}
Option name | Type | Description |
---|---|---|
component | Component | an instance of Component class or subclass |
deepCopy | Boolean | optional |
return | Component |
Component class method
Create a copy of component, including a copy of DOM element. Returns a copy of component
(of the same class) with new DOM element (not inserted into page).
Component is added to the same scope as the original component.
function Component$$copy(component, deepCopy) {
check(component, Component);
check(deepCopy, Match.Optional(Boolean));
if (deepCopy && !component.container)
throw new Error('Cannot deep copy component without container facet');
// copy DOM element, using Dom facet if it is available
var newEl = component.dom
? component.dom.copy(deepCopy)
: component.el.cloneNode(deepCopy);
var ComponentClass = component.constructor;
// create component of the same class on the element
var aComponent = ComponentClass.createOnElement(newEl, undefined, component.scope, component.extraFacets);
var state = component._getState(deepCopy || false);
aComponent.setState(state);
_.deferMethod(aComponent, 'broadcast', 'stateready');
return aComponent;
}
Option name | Type | Description |
---|---|---|
el | Element | optional element to attach component to. If element is not passed, it will be created |
innerHTML | String | optional inner html to insert in element before binding. |
rootScope | Scope | optional scope to put component in. If not passed, component will be attached to the scope that contains the element. If such scope does not exist, new scope will be created. |
extraFacets | Array.<String> | list of extra facet to add to component |
return | Subclass.<Component> |
Component class method
Creates an instance of component atached to element. All subclasses of component inherit this method.
Returns the component of the class this method is used with (thecontext of the method call).
function Component$$createOnElement(el, innerHTML, rootScope, extraFacets) {
check(innerHTML, Match.Optional(String));
check(rootScope, Match.Optional(Scope));
check(extraFacets, Match.Optional([String]));
// "this" refers to the class of component here, as this is a class method
if (el && innerHTML) el.innerHTML = innerHTML;
el = el || _createComponentElement.call(this, innerHTML);
rootScope = rootScope || _findOrCreateComponentRootScope(el);
var aComponent = _addAttributeAndBindComponent.call(this, el, rootScope, extraFacets);
aComponent.broadcast('stateready');
return aComponent;
}
function _createComponentElement(innerHTML) {
// "this" refers to the class of component here, as this is a class method
var Dom = facetsRegistry.get('Dom')
, domFacetConfig = this.getFacetConfig('dom')
, templateFacetConfig = this.getFacetConfig('template')
, template = templateFacetConfig && templateFacetConfig.template;
var elConfig = {
domConfig: domFacetConfig,
template: template,
content: innerHTML
};
return Dom.createElement(elConfig);
}
function _findOrCreateComponentRootScope(el) {
var parent = Component.getContainingComponent(el, false, 'Container');
return parent ? parent.container.scope : new Scope(el);
}
function _addAttributeAndBindComponent(el, rootScope, extraFacets) {
// add bind attribute to element
var attr = new BindAttribute(el);
// "this" refers to the class of component here, as this is a class method
attr.compClass = this.name;
attr.compFacets = extraFacets;
attr.decorate();
// should be required here to resolve circular dependency
var miloBinder = require('../binder');
miloBinder(el, rootScope);
return rootScope[attr.compName];
}
Option name | Type | Description |
---|---|---|
state | Object | state from which component will be created |
rootScope | Scope | scope to which component will be added |
newUniqueName | Boolean | optional |
throwOnErrors | Boolean | If set to false, then errors will only be logged to console. True by default. |
return | Component | component |
Component class method
Creates component from component state, that includes information about its class, extra facets, facets data and all scope children.
This is used to save/load, copy/paste and drag/drop component
function Component$$createFromState(state, rootScope, newUniqueName, throwOnErrors) {
check(state, Match.ObjectIncluding({
compName: Match.Optional(String),
compClass: Match.Optional(String),
extraFacets: Match.Optional([String]),
facetsStates: Match.Optional(Object),
outerHTML: String
}));
var miloBinder = require('../binder');
// create wrapper element optionally renaming component
var wrapEl = _createComponentWrapElement(state, newUniqueName);
// instantiate all components from HTML
var scope = miloBinder(wrapEl, undefined, undefined, throwOnErrors);
// as there should only be one component, call to _any will return it
var component = scope._any();
// set component's scope
if (rootScope) {
component.scope = rootScope;
rootScope._add(component);
}
// restore component state
component.setState(state);
_.deferMethod(component, 'broadcast', 'stateready');
return component;
}
// used by Component$$createFromState
function _createComponentWrapElement(state, newUniqueName) {
var wrapEl = document.createElement('div');
wrapEl.innerHTML = state.outerHTML;
var children = domUtils.children(wrapEl);
if (children.length != 1)
throw new Error('cannot create component: incorrect HTML, elements number: ' + children.length + ' (should be 1)');
var compEl = children[0];
var attr = new BindAttribute(compEl);
attr.compName = newUniqueName ? miloComponentName() : state.compName;
attr.compClass = state.compClass;
attr.compFacets = state.extraFacets;
attr.decorate();
return wrapEl;
}
Option name | Type | Description |
---|---|---|
dataTransfer | DataTransfer | Data transfer |
Creates a component from a DataTransfer object (if possible)
function Component$$createFromDataTransfer(dataTransfer) {
var dataType = _.find(dataTransfer.types, function (type) {
return COMPONENT_DATA_TYPE_REGEX.test(type);
});
if (!dataType) return;
var state = _.jsonParse(dataTransfer.getData(dataType));
if (!state) return;
return Component.createFromState(state, undefined, true);
}
Option name | Type | Description |
---|---|---|
scope | Scope | scope to which component will belong. It is usually a top level scope object returned by |
element | Element | DOM element that component is attached to |
name | String | component name, should be unique in the scope of component |
componentInfo | ComponentInfo | instance of ComponentInfo class that can be used to create a copy of component TODO try removing it |
Component instance method.
Initializes component. Automatically called by inherited constructor of FacetedObject.
Subclasses should call inherited init methods:
Component.prototype.init.apply(this, arguments)
function Component$init(scope, element, name, componentInfo) {
// create DOM element if it wasn't passed to Constructor
this.el = element || this.createElement();
// store reference to component on DOM element
if (this.el) {
// check that element does not have a component already atached
var elComp = this.el[config.componentRef];
if (elComp)
logger.warn('component ' + name + ' attached to element that already has component ' + elComp.name);
this.el[config.componentRef] = this;
}
_.defineProperties(this, {
componentInfo: componentInfo,
extraFacets: []
}, _.ENUM);
this.name = name;
this.scope = scope;
// create component messenger
var messenger = new Messenger(this);
_.defineProperty(this, MESSENGER_PROPERTY, messenger);
// check all facets dependencies (required facets)
this.allFacets('check');
// start all facets
this.allFacets('start');
// call start method if it's defined in subclass
if (this.start) this.start();
}
This is a stub to avoid confusion whether the method of superclass should be called in subclasses
The start method of subclass instance is called once all the facets are created, initialized and started (see above)
function Component$start() {}
Component instance method.
Initializes the element which this component is bound to
This method is called when a component is instantiated outside the DOM and
will generate a new element for the component.
function Component$createElement() {
if (typeof document == 'undefined')
return;
this.el = this.dom
? this.dom.createElement()
: document.createElement('DIV');
return this.el;
}
Option name | Type | Description |
---|---|---|
facetNameOrClass | Function, String | |
return | Boolean |
Component instance method
Returns true if component has facet
function Component$hasFacet(facetNameOrClass) {
var facetName = _.firstLowerCase(typeof facetNameOrClass == 'function'
? facetNameOrClass.name
: facetNameOrClass);
var facet = this[facetName];
if (! facet instanceof ComponentFacet)
logger.warn('expected facet', facetName, 'but this property name is used for something else');
return !! facet;
}
Option name | Type | Description |
---|---|---|
facetNameOrClass | String, Subclass.<Component> | name of facet class or the class itself. If name is passed, the class will be retireved from facetsRegistry |
facetConfig | Object | optional facet configuration |
facetName | String | optional facet name. Allows to add facet under a name different from the class name supplied. |
throwOnErrors | Boolean | If set to false, then errors will only be logged to console. True by default. |
Component instance method.
Adds facet with given name or class to the instance of Component (or its subclass).
function Component$addFacet(facetNameOrClass, facetConfig, facetName, throwOnErrors) {
check(facetNameOrClass, Match.OneOf(String, Match.Subclass(ComponentFacet)));
check(facetConfig, Match.Optional(Object));
check(facetName, Match.Optional(String));
var FacetClass;
// if only name passed, retrieve facet class from registry
if (typeof facetNameOrClass == 'string') {
var facetClassName = _.firstUpperCase(facetNameOrClass);
FacetClass = facetsRegistry.get(facetClassName);
} else
FacetClass = facetNameOrClass;
if (!facetName)
facetName = _.firstLowerCase(FacetClass.name);
this.extraFacets.push(facetName);
// add facet using method of FacetedObject
var newFacet = FacetedObject.prototype.addFacet.call(this, FacetClass, facetConfig, facetName, throwOnErrors);
// check depenedencies and start facet
if (newFacet.check) newFacet.check();
if (newFacet.start) newFacet.start();
}
Option name | Type | Description |
---|---|---|
method | String | method name to envoke on the facet |
return | Object |
Component instance method.
Envoke given method with optional parameters on all facets.
Returns the map of values returned by all facets. If the facet doesn't have the method it is simply not called and the value in the map will be undefined.
function Component$allFacets(method) { // ,... arguments
var args = _.slice(arguments, 1);
return _.mapKeys(this.facets, function(facet, fctName) {
if (facet && typeof facet[method] == 'function')
return facet[method].apply(facet, args);
});
}
Option name | Type | Description |
---|---|---|
[name] | String | optional new name of component, |
[renameInScope] | Boolean | optional false to not rename ComponentInfo object in its scope, true by default |
Component instance method.
function Component$rename(name, renameInScope) {
name = name || miloComponentName();
this.componentInfo.rename(name, false);
Scope.rename(this, name, renameInScope);
}
Option name | Type | Description |
---|---|---|
preserveScopeProperty | Boolean | true not to delete scope property of component |
quiet | Boolean | optional true to suppress the warning message if the component is not in scope |
Component instance method.
Removes component from its scope.
function Component$remove(preserveScopeProperty, quiet) {
if (this.scope) {
this.scope._remove(this.name, quiet);
if (! preserveScopeProperty)
delete this.scope;
}
}
Option name | Type | Description |
---|---|---|
parentEl | HTMLElement | The element into which the component should be inserted. |
referenceEl | HTMLElement | (optional) The reference element it should be inserted before. |
Component instance method.
Inserts the component into the DOM and attempts to adjust the scope tree accordingly.
function Component$insertInto(parentEl, referenceEl) {
parentEl.insertBefore(this.el, referenceEl);
this.setScopeParentFromDOM();
}
Component instance method
Retrieves all component state, including information about its class, extra facets, facets data and all scope children.
This information is used to save/load, copy/paste and drag/drop component
Returns component state
function Component$getState() {
this.broadcast('getstatestarted', { rootComponent: this }, undefined, true);
var state = this._getState(true);
state.outerHTML = this.el.outerHTML;
_.deferMethod(this, 'broadcast', 'getstatecompleted', { rootComponent: this }, undefined, true);
return state;
}
Option name | Type | Description |
---|---|---|
options | Object | can be used by subclasses. |
return | Object |
Component instance method
Retrieves all component state, including information about its class, extra facets, facets data and all scope children.
This information is used to save/load, copy/paste and drag/drop component
If component has Transfer facet on it, this method retrieves state from this facet
Returns component state
function Component$getTransferState(options) {
return this.transfer
? this.transfer.getState(options)
: this.getState(options);
}
Option name | Type | Description |
---|---|---|
conditionOrFacet | Function, String | optional condition that component should pass (or facet name it should contain) |
return | Component |
Component instance method.
Returns the scope parent of a component.
If conditionOrFacet
parameter is not specified, an immediate parent will be returned, otherwise the closest ancestor with a specified facet or passing condition test.
function Component$getScopeParent(conditionOrFacet) {
return _callGetScopeParent.call(this, _getScopeParent, conditionOrFacet);
}
function _callGetScopeParent(_getScopeParentFunc, conditionOrFacet) {
check(conditionOrFacet, Match.Optional(Match.OneOf(Function, String)));
var conditionFunc = componentUtils._makeComponentConditionFunc(conditionOrFacet);
return _getScopeParentFunc.call(this, conditionFunc);
}
function _getScopeParent(conditionFunc) {
var parent;
try { parent = this.scope._hostObject.owner; } catch(e) {}
// Where there is no parent, this function will return undefined
// The parent component is checked recursively
if (parent) {
if (! conditionFunc || conditionFunc(parent) )
return parent;
else
return _getScopeParent.call(parent, conditionFunc);
}
}
Option name | Type | Description |
---|---|---|
[ComponentClass] | Function | component class that the parent should have, same class by default |
return | Component |
Component instance method
Returns scope parent with a given class, with same class if not specified
function Component$getScopeParentWithClass(ComponentClass) {
ComponentClass = ComponentClass || this.constructor;
return _getScopeParent.call(this, function(comp) {
return comp instanceof ComponentClass;
});
}
Option name | Type | Description |
---|---|---|
conditionOrFacet | Function, String | optional condition that component should pass (or facet name it should contain) |
return | Component |
Component instance method.
Returns the topmost scope parent of a component.
If conditionOrFacet
parameter is not specified, the topmost scope parent will be returned, otherwise the topmost ancestor with a specified facet or passing condition test.
function Component$getTopScopeParent(conditionOrFacet) {
return _callGetScopeParent.call(this, _getTopScopeParent, conditionOrFacet);
}
function _getTopScopeParent(conditionFunc) {
var topParent
, parent = this;
do {
parent = _getScopeParent.call(parent, conditionFunc);
if (parent)
topParent = parent;
} while (parent);
return topParent;
}
Option name | Type | Description |
---|---|---|
[ComponentClass] | Function | component class that the parent should have, same class by default |
return | Component |
Component instance method
Returns scope parent with a given class, with same class if not specified
function Component$getTopScopeParentWithClass(ComponentClass) {
ComponentClass = ComponentClass || this.constructor;
return _getTopScopeParent.call(this, function(comp) {
return comp instanceof ComponentClass;
});
}
Component instance method
Finds scope parent of component using DOM tree (unlike getScopeParent that simply goes up the scope tree).
While getScopeParent is faster it may fail if scope chain is not setup yet (e.g., when component has been just inserted).
The scope property of component will be changed to point to scope object of container facet of that parent.
Returned scope parent of the component will be undefined (as well as component's scope property) if no parent in the DOM tree has container facet.
TODO Method will not bind DOM children correctly if component has no container facet.
function Component$setScopeParentFromDOM() {
var parentEl = this.el.parentNode;
var parent, foundParent;
while (parentEl && ! foundParent) {
parent = Component.getComponent(parentEl);
foundParent = parent && parent.container;
parentEl = parentEl.parentNode;
}
this.remove(); // remove component from its current scope (if it is defined)
if (foundParent) {
this.rename(undefined, false);
parent.container.scope._add(this);
return parent;
}
}
Option name | Type | Description |
---|---|---|
callback | ||
thisArg |
Walks component tree, calling provided callback on each component
function Component$walkScopeTree(callback, thisArg) {
callback.call(thisArg, this);
if (!this.container) return;
this.container.scope._each(function(component) {
component.walkScopeTree(callback, thisArg);
});
}
function Component$treePathOf(component) {
return domUtils.treePathOf(this.el, component.el);
}
function Component$getComponentAtTreePath(treePath, nearest) {
var node = domUtils.getNodeAtTreePath(this.el, treePath, nearest);
return Component.getComponent(node);
}
function Component$insertAtTreePath(treePath, component, nearest) {
var wasInserted = domUtils.insertAtTreePath(this.el, treePath, component.el);
if (wasInserted) component.setScopeParentFromDOM();
return wasInserted;
}
Option name | Type | Description |
---|---|---|
msg | String, RegExp | message to be sent |
[data] | Any | optional message data |
[callback] | Function | optional callback |
[synchronously] | Boolean | if it should use postMessageSync |
Broadcast message to component and to all its scope children
function Component$broadcast(msg, data, callback, synchronously) {
var postMethod = synchronously ? 'postMessageSync' : 'postMessage';
this.walkScopeTree(function(component) {
component[postMethod](msg, data, callback);
});
}
Destroy component: removes component from DOM, removes it from scope, deletes all references to DOM nodes and unsubscribes from all messages both component and all facets
function Component$destroy(opts) {
if (typeof opts == 'boolean') opts = { quiet: opts };
else if (!opts) opts = {};
if (this._destroyed) {
if (!opts.quiet) logger.warn('Component destroy: component is already destroyed');
return;
}
this.remove(false, opts.quiet);
this.allFacets('destroy', opts);
this[MESSENGER_PROPERTY].destroy();
if (this.el) {
domUtils.detachComponent(this.el);
domUtils.removeElement(this.el);
delete this.el;
}
this.componentInfo.destroy();
this._destroyed = true;
}
Returns true if component was destroyed
function Component$isDestroyed() {
return !!this._destroyed;
}