infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
985 lines (897 loc) • 125 kB
JavaScript
/*
Copyright 2011-2016 OCAD University
Copyright 2010-2011 Lucendo Development Ltd.
Copyright 2012-2014 Raising the Floor - US
Copyright 2014-2016 Raising the Floor - International
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
*/
var fluid_2_0_0 = fluid_2_0_0 || {};
(function ($, fluid) {
"use strict";
/** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
/** The Fluid "IoC System proper" - resolution of references and
* completely automated instantiation of declaratively defined
* component trees */
// Currently still uses manual traversal - once we ban manually instantiated components,
// it will use the instantiator's records instead.
fluid.visitComponentChildren = function (that, visitor, options, segs) {
segs = segs || [];
for (var name in that) {
var component = that[name];
// This entire algorithm is primitive and expensive and will be removed once we can abolish manual init components
if (!fluid.isComponent(component) || (options.visited && options.visited[component.id])) {
continue;
}
segs.push(name);
if (options.visited) { // recall that this is here because we may run into a component that has been cross-injected which might otherwise cause cyclicity
options.visited[component.id] = true;
}
if (visitor(component, name, segs, segs.length - 1)) {
return true;
}
if (!options.flat) {
fluid.visitComponentChildren(component, visitor, options, segs);
}
segs.pop();
}
};
fluid.getContextHash = function (instantiator, that) {
var shadow = instantiator.idToShadow[that.id];
return shadow && shadow.contextHash;
};
fluid.componentHasGrade = function (that, gradeName) {
var contextHash = fluid.getContextHash(fluid.globalInstantiator, that);
return !!(contextHash && contextHash[gradeName]);
};
// A variant of fluid.visitComponentChildren that supplies the signature expected for fluid.matchIoCSelector
// this is: thatStack, contextHashes, memberNames, i - note, the supplied arrays are NOT writeable and shared through the iteration
fluid.visitComponentsForMatching = function (that, options, visitor) {
var instantiator = fluid.getInstantiator(that);
options = $.extend({
visited: {},
instantiator: instantiator
}, options);
var thatStack = [that];
var contextHashes = [fluid.getContextHash(instantiator, that)];
var visitorWrapper = function (component, name, segs) {
thatStack.length = 1;
contextHashes.length = 1;
for (var i = 0; i < segs.length; ++i) {
var child = thatStack[i][segs[i]];
thatStack[i + 1] = child;
contextHashes[i + 1] = fluid.getContextHash(instantiator, child) || {};
}
return visitor(component, thatStack, contextHashes, segs, segs.length);
};
fluid.visitComponentChildren(that, visitorWrapper, options, []);
};
fluid.getMemberNames = function (instantiator, thatStack) {
var path = instantiator.idToPath(thatStack[thatStack.length - 1].id);
var segs = instantiator.parseEL(path);
// TODO: we should now have no longer shortness in the stack
segs.unshift.apply(segs, fluid.generate(thatStack.length - segs.length, ""));
return segs;
};
// thatStack contains an increasing list of MORE SPECIFIC thats.
// this visits all components starting from the current location (end of stack)
// in visibility order UP the tree.
fluid.visitComponentsForVisibility = function (instantiator, thatStack, visitor, options) {
options = options || {
visited: {},
flat: true,
instantiator: instantiator
};
var memberNames = fluid.getMemberNames(instantiator, thatStack);
for (var i = thatStack.length - 1; i >= 0; --i) {
var that = thatStack[i];
// explicitly visit the direct parent first
options.visited[that.id] = true;
if (visitor(that, memberNames[i], memberNames, i)) {
return;
}
if (fluid.visitComponentChildren(that, visitor, options, memberNames)) {
return;
}
memberNames.pop();
}
};
fluid.mountStrategy = function (prefix, root, toMount) {
var offset = prefix.length;
return function (target, name, i, segs) {
if (i <= prefix.length) { // Avoid OOB to not trigger deoptimisation!
return;
}
for (var j = 0; j < prefix.length; ++j) {
if (segs[j] !== prefix[j]) {
return;
}
}
return toMount(target, name, i - prefix.length, segs.slice(offset));
};
};
fluid.invokerFromRecord = function (invokerec, name, that) {
fluid.pushActivity("makeInvoker", "beginning instantiation of invoker with name %name and record %record as child of %that",
{name: name, record: invokerec, that: that});
var invoker = invokerec ? fluid.makeInvoker(that, invokerec, name) : undefined;
fluid.popActivity();
return invoker;
};
fluid.memberFromRecord = function (memberrecs, name, that) {
var togo;
for (var i = 0; i < memberrecs.length; ++i) { // memberrecs is the special "fluid.mergingArray" type which is not Arrayable
var expanded = fluid.expandImmediate(memberrecs[i], that);
if (!fluid.isPlainObject(togo)) { // poor man's "merge" algorithm to hack FLUID-5668 for now
togo = expanded;
} else {
togo = $.extend(true, togo, expanded);
}
}
return togo;
};
fluid.recordStrategy = function (that, options, optionsStrategy, recordPath, recordMaker, prefix, exceptions) {
prefix = prefix || [];
return {
strategy: function (target, name, i) {
if (i !== 1) {
return;
}
var record = fluid.driveStrategy(options, [recordPath, name], optionsStrategy);
if (record === undefined) {
return;
}
fluid.set(target, [name], fluid.inEvaluationMarker);
var member = recordMaker(record, name, that);
fluid.set(target, [name], member);
return member;
},
initter: function () {
var records = fluid.driveStrategy(options, recordPath, optionsStrategy) || {};
for (var name in records) {
if (!exceptions || !exceptions[name]) {
fluid.getForComponent(that, prefix.concat([name]));
}
}
}
};
};
// patch Fluid.js version for timing
fluid.instantiateFirers = function (that) {
var shadow = fluid.shadowForComponent(that);
var initter = fluid.get(shadow, ["eventStrategyBlock", "initter"]) || fluid.identity;
initter();
};
fluid.makeDistributionRecord = function (contextThat, sourceRecord, sourcePath, targetSegs, exclusions, sourceType) {
sourceType = sourceType || "distribution";
fluid.pushActivity("makeDistributionRecord", "Making distribution record from source record %sourceRecord path %sourcePath to target path %targetSegs", {sourceRecord: sourceRecord, sourcePath: sourcePath, targetSegs: targetSegs});
var source = fluid.copy(fluid.get(sourceRecord, sourcePath));
fluid.each(exclusions, function (exclusion) {
fluid.model.applyChangeRequest(source, {segs: exclusion, type: "DELETE"});
});
var record = {options: {}};
fluid.model.applyChangeRequest(record, {segs: targetSegs, type: "ADD", value: source});
fluid.checkComponentRecord(record);
fluid.popActivity();
return $.extend(record, {contextThat: contextThat, recordType: sourceType});
};
// Part of the early "distributeOptions" workflow. Given the description of the blocks to be distributed, assembles "canned" records
// suitable to be either registered into the shadow record for later or directly pushed to an existing component, as well as honouring
// any "removeSource" annotations by removing these options from the source block.
fluid.filterBlocks = function (contextThat, sourceBlocks, sourceSegs, targetSegs, exclusions, removeSource) {
var togo = [];
fluid.each(sourceBlocks, function (block) {
var source = fluid.get(block.source, sourceSegs);
if (source) {
togo.push(fluid.makeDistributionRecord(contextThat, block.source, sourceSegs, targetSegs, exclusions, block.recordType));
var rescued = $.extend({}, source);
if (removeSource) {
fluid.model.applyChangeRequest(block.source, {segs: sourceSegs, type: "DELETE"});
}
fluid.each(exclusions, function (exclusion) {
var orig = fluid.get(rescued, exclusion);
fluid.set(block.source, sourceSegs.concat(exclusion), orig);
});
}
});
return togo;
};
// Use this peculiar signature since the actual component and shadow itself may not exist yet. Perhaps clean up with FLUID-4925
fluid.noteCollectedDistribution = function (parentShadow, memberName, distribution) {
fluid.model.setSimple(parentShadow, ["collectedDistributions", memberName, distribution.id], true);
};
fluid.isCollectedDistribution = function (parentShadow, memberName, distribution) {
return fluid.model.getSimple(parentShadow, ["collectedDistributions", memberName, distribution.id]);
};
fluid.clearCollectedDistributions = function (parentShadow, memberName) {
fluid.model.applyChangeRequest(parentShadow, {segs: ["collectedDistributions", memberName], type: "DELETE"});
};
fluid.collectDistributions = function (distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i) {
var lastMember = memberNames[memberNames.length - 1];
if (!fluid.isCollectedDistribution(parentShadow, lastMember, distribution) &&
fluid.matchIoCSelector(distribution.selector, thatStack, contextHashes, memberNames, i)) {
distributedBlocks.push.apply(distributedBlocks, distribution.blocks);
fluid.noteCollectedDistribution(parentShadow, lastMember, distribution);
}
};
// Slightly silly function to clean up the "appliedDistributions" records. In general we need to be much more aggressive both
// about clearing instantiation garbage (e.g. onCreate and most of the shadow)
// as well as caching frequently-used records such as the "thatStack" which
// would mean this function could be written in a sensible way
fluid.registerCollectedClearer = function (shadow, parentShadow, memberName) {
if (!shadow.collectedClearer && parentShadow) {
shadow.collectedClearer = function () {
fluid.clearCollectedDistributions(parentShadow, memberName);
};
}
};
fluid.receiveDistributions = function (parentThat, gradeNames, memberName, that) {
var instantiator = fluid.getInstantiator(parentThat || that);
var thatStack = instantiator.getThatStack(parentThat || that); // most specific is at end
thatStack.unshift(fluid.rootComponent);
var memberNames = fluid.getMemberNames(instantiator, thatStack);
var shadows = fluid.transform(thatStack, function (thisThat) {
return instantiator.idToShadow[thisThat.id];
});
var parentShadow = shadows[shadows.length - (parentThat ? 1 : 2)];
var contextHashes = fluid.getMembers(shadows, "contextHash");
if (parentThat) { // if called before construction of component from assembleCreatorArguments - NB this path will be abolished/amalgamated
memberNames.push(memberName);
contextHashes.push(fluid.gradeNamesToHash(gradeNames));
thatStack.push(that);
} else {
fluid.registerCollectedClearer(shadows[shadows.length - 1], parentShadow, memberNames[memberNames.length - 1]);
}
var distributedBlocks = [];
for (var i = 0; i < thatStack.length - 1; ++i) {
fluid.each(shadows[i].distributions, function (distribution) { // eslint-disable-line no-loop-func
fluid.collectDistributions(distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i);
});
}
return distributedBlocks;
};
fluid.computeTreeDistance = function (path1, path2) {
var i = 0;
while (i < path1.length && i < path2.length && path1[i] === path2[i]) {
++i;
}
return path1.length + path2.length - 2*i; // eslint-disable-line space-infix-ops
};
// Called from applyDistributions (immediate application route) as well as mergeRecordsToList (pre-instantiation route) AS WELL AS assembleCreatorArguments (pre-pre-instantiation route)
fluid.computeDistributionPriority = function (targetThat, distributedBlock) {
if (!distributedBlock.priority) {
var instantiator = fluid.getInstantiator(targetThat);
var targetStack = instantiator.getThatStack(targetThat);
var targetPath = fluid.getMemberNames(instantiator, targetStack);
var sourceStack = instantiator.getThatStack(distributedBlock.contextThat);
var sourcePath = fluid.getMemberNames(instantiator, sourceStack);
var distance = fluid.computeTreeDistance(targetPath, sourcePath);
distributedBlock.priority = fluid.mergeRecordTypes.distribution - distance;
}
return distributedBlock;
};
// convert "preBlocks" as produced from fluid.filterBlocks into "real blocks" suitable to be used by the expansion machinery.
fluid.applyDistributions = function (that, preBlocks, targetShadow) {
var distributedBlocks = fluid.transform(preBlocks, function (preBlock) {
return fluid.generateExpandBlock(preBlock, that, targetShadow.mergePolicy);
}, function (distributedBlock) {
return fluid.computeDistributionPriority(that, distributedBlock);
});
var mergeOptions = targetShadow.mergeOptions;
mergeOptions.mergeBlocks.push.apply(mergeOptions.mergeBlocks, distributedBlocks);
mergeOptions.updateBlocks();
return distributedBlocks;
};
// TODO: This implementation is obviously poor and has numerous flaws - in particular it does no backtracking as well as matching backwards through the selector
fluid.matchIoCSelector = function (selector, thatStack, contextHashes, memberNames, i) {
var thatpos = thatStack.length - 1;
var selpos = selector.length - 1;
while (true) {
var mustMatchHere = thatpos === thatStack.length - 1 || selector[selpos].child;
var that = thatStack[thatpos];
var selel = selector[selpos];
var match = true;
for (var j = 0; j < selel.predList.length; ++j) {
var pred = selel.predList[j];
if (pred.context && !(contextHashes[thatpos][pred.context] || memberNames[thatpos] === pred.context)) {
match = false;
break;
}
if (pred.id && that.id !== pred.id) {
match = false;
break;
}
}
if (selpos === 0 && thatpos > i && mustMatchHere) {
match = false; // child selector must exhaust stack completely - FLUID-5029
}
if (match) {
if (selpos === 0) {
return true;
}
--thatpos;
--selpos;
}
else {
if (mustMatchHere) {
return false;
}
else {
--thatpos;
}
}
if (thatpos < i) {
return false;
}
}
};
/** Query for all components matching a selector in a particular tree
* @param root {Component} The root component at which to start the search
* @param selector {String} An IoCSS selector, in form of a string. Note that since selectors supplied to this function implicitly
* match downwards, they need not contain the "head context" followed by whitespace required in the distributeOptions form. E.g.
* simply <code>"fluid.viewComponent"</code> will match all viewComponents below the root.
* @param flat {Boolean} [Optional] <code>true</code> if the search should just be performed at top level of the component tree
* Note that with <code>flat=true</code> this search will scan every component in the tree and may well be very slow.
*/
// supported, PUBLIC API function
fluid.queryIoCSelector = function (root, selector, flat) {
var parsed = fluid.parseSelector(selector, fluid.IoCSSMatcher);
var togo = [];
fluid.visitComponentsForMatching(root, {flat: flat}, function (that, thatStack, contextHashes, memberNames, i) {
if (fluid.matchIoCSelector(parsed, thatStack, contextHashes, memberNames, i)) {
togo.push(that);
}
});
return togo;
};
fluid.isIoCSSSelector = function (context) {
return context.indexOf(" ") !== -1; // simple-minded check for an IoCSS reference
};
fluid.pushDistributions = function (targetHead, selector, target, blocks) {
var targetShadow = fluid.shadowForComponent(targetHead);
var id = fluid.allocateGuid();
var distribution = {
id: id, // This id is used in clearDistributions
target: target, // Here for improved debuggability - info is duplicated in "selector"
selector: selector,
blocks: blocks
};
Object.freeze(distribution);
Object.freeze(distribution.blocks);
fluid.pushArray(targetShadow, "distributions", distribution);
return id;
};
fluid.clearDistribution = function (targetHead, id) {
var targetShadow = fluid.shadowForComponent(targetHead);
fluid.remove_if(targetShadow.distributions, function (distribution) {
return distribution.id === id;
});
};
fluid.clearDistributions = function (shadow) {
fluid.each(shadow.outDistributions, function (outDist) {
fluid.clearDistribution(outDist.targetComponent, outDist.distributionId);
});
};
// Modifies a parsed selector to extract and remove its head context which will be matched upwards
fluid.extractSelectorHead = function (parsedSelector) {
var predList = parsedSelector[0].predList;
var context = predList[0].context;
predList.length = 0;
return context;
};
fluid.parseExpectedOptionsPath = function (path, role) {
var segs = fluid.model.parseEL(path);
if (segs[0] !== "options") {
fluid.fail("Error in options distribution path ", path, " - only " + role + " paths beginning with \"options\" are supported");
}
return segs.slice(1);
};
fluid.replicateProperty = function (source, property, targets) {
if (source[property] !== undefined) {
fluid.each(targets, function (target) {
target[property] = source[property];
});
}
};
fluid.undistributableOptions = ["gradeNames", "distributeOptions", "argumentMap", "initFunction", "mergePolicy", "progressiveCheckerOptions"]; // automatically added to "exclusions" of every distribution
fluid.distributeOptions = function (that, optionsStrategy) {
var thatShadow = fluid.shadowForComponent(that);
var records = fluid.driveStrategy(that.options, "distributeOptions", optionsStrategy);
fluid.each(records, function distributeOptionsOne(record) {
fluid.pushActivity("distributeOptions", "parsing distributeOptions block %record %that ", {that: that, record: record});
if (typeof(record.target) !== "string") {
fluid.fail("Error in options distribution record ", record, " a member named \"target\" must be supplied holding an IoC reference");
}
if (typeof(record.source) === "string" ^ record.record === undefined) {
fluid.fail("Error in options distribution record ", record, ": must supply either a member \"source\" holding an IoC reference or a member \"record\" holding a literal record");
}
var targetRef = fluid.parseContextReference(record.target);
var targetComp, selector, context;
if (fluid.isIoCSSSelector(targetRef.context)) {
selector = fluid.parseSelector(targetRef.context, fluid.IoCSSMatcher);
var headContext = fluid.extractSelectorHead(selector);
if (headContext === "/") {
targetComp = fluid.rootComponent;
} else {
context = headContext;
}
}
else {
context = targetRef.context;
}
targetComp = targetComp || fluid.resolveContext(context, that);
if (!targetComp) {
fluid.fail("Error in options distribution record ", record, " - could not resolve context {" + context + "} to a root component");
}
var targetSegs = fluid.model.parseEL(targetRef.path);
var preBlocks;
if (record.record !== undefined) {
preBlocks = [(fluid.makeDistributionRecord(that, record.record, [], targetSegs, []))];
}
else {
var source = fluid.parseContextReference(record.source);
if (source.context !== "that") {
fluid.fail("Error in options distribution record ", record, " only a context of {that} is supported");
}
var sourceSegs = fluid.parseExpectedOptionsPath(source.path, "source");
var fullExclusions = fluid.makeArray(record.exclusions).concat(sourceSegs.length === 0 ? fluid.undistributableOptions : []);
var exclusions = fluid.transform(fullExclusions, function (exclusion) {
return fluid.model.parseEL(exclusion);
});
preBlocks = fluid.filterBlocks(that, thatShadow.mergeOptions.mergeBlocks, sourceSegs, targetSegs, exclusions, record.removeSource);
thatShadow.mergeOptions.updateBlocks(); // perhaps unnecessary
}
fluid.replicateProperty(record, "priority", preBlocks);
fluid.replicateProperty(record, "namespace", preBlocks);
// TODO: inline material has to be expanded in its original context!
if (selector) {
var distributionId = fluid.pushDistributions(targetComp, selector, record.target, preBlocks);
thatShadow.outDistributions = thatShadow.outDistributions || [];
thatShadow.outDistributions.push({
targetComponent: targetComp,
distributionId: distributionId
});
}
else { // The component exists now, we must rebalance it
var targetShadow = fluid.shadowForComponent(targetComp);
fluid.applyDistributions(that, preBlocks, targetShadow);
}
fluid.popActivity();
});
};
fluid.gradeNamesToHash = function (gradeNames) {
var contextHash = {};
fluid.each(gradeNames, function (gradeName) {
contextHash[gradeName] = true;
contextHash[fluid.computeNickName(gradeName)] = true;
});
return contextHash;
};
fluid.cacheShadowGrades = function (that, shadow) {
var contextHash = fluid.gradeNamesToHash(that.options.gradeNames);
if (!contextHash[shadow.memberName]) {
contextHash[shadow.memberName] = "memberName"; // This is filtered out again in recordComponent - TODO: Ensure that ALL resolution uses the scope chain eventually
}
shadow.contextHash = contextHash;
fluid.each(contextHash, function (troo, context) {
shadow.ownScope[context] = that;
if (shadow.parentShadow && shadow.parentShadow.that.type !== "fluid.rootComponent") {
shadow.parentShadow.childrenScope[context] = that;
}
});
};
// First sequence point where the mergeOptions strategy is delivered from Fluid.js - here we take care
// of both receiving and transmitting options distributions
fluid.deliverOptionsStrategy = function (that, target, mergeOptions) {
var shadow = fluid.shadowForComponent(that, shadow);
fluid.cacheShadowGrades(that, shadow);
shadow.mergeOptions = mergeOptions;
};
/** Dynamic grade closure algorithm - the following 4 functions share access to a small record structure "rec" which is
* constructed at the start of fluid.computeDynamicGrades
*/
fluid.collectDistributedGrades = function (rec) {
// Receive distributions first since these may cause arrival of more contextAwareness blocks.
var distributedBlocks = fluid.receiveDistributions(null, null, null, rec.that);
if (distributedBlocks.length > 0) {
var readyBlocks = fluid.applyDistributions(rec.that, distributedBlocks, rec.shadow);
var gradeNamesList = fluid.transform(fluid.getMembers(readyBlocks, ["source", "gradeNames"]), fluid.makeArray);
fluid.accumulateDynamicGrades(rec, fluid.flatten(gradeNamesList));
}
};
// Apply a batch of freshly acquired plain dynamic grades to the target component and recompute its options
fluid.applyDynamicGrades = function (rec) {
rec.oldGradeNames = fluid.makeArray(rec.gradeNames);
// Note that this crude algorithm doesn't allow us to determine which grades are "new" and which not // TODO: can no longer interpret comment
var newDefaults = fluid.copy(fluid.getMergedDefaults(rec.that.typeName, rec.gradeNames));
rec.gradeNames.length = 0; // acquire derivatives of dynamic grades (FLUID-5054)
rec.gradeNames.push.apply(rec.gradeNames, newDefaults.gradeNames);
fluid.each(rec.gradeNames, function (gradeName) {
if (!fluid.isIoCReference(gradeName)) {
rec.seenGrades[gradeName] = true;
}
});
var shadow = rec.shadow;
fluid.cacheShadowGrades(rec.that, shadow);
// This cheap strategy patches FLUID-5091 for now - some more sophisticated activity will take place
// at this site when we have a full fix for FLUID-5028
shadow.mergeOptions.destroyValue(["mergePolicy"]);
shadow.mergeOptions.destroyValue(["components"]);
shadow.mergeOptions.destroyValue(["invokers"]);
rec.defaultsBlock.source = newDefaults;
shadow.mergeOptions.updateBlocks();
shadow.mergeOptions.computeMergePolicy(); // TODO: we should really only do this if its content changed - this implies moving all options evaluation over to some (cheap) variety of the ChangeApplier
fluid.accumulateDynamicGrades(rec, newDefaults.gradeNames);
};
// Filter some newly discovered grades into their plain and dynamic queues
fluid.accumulateDynamicGrades = function (rec, newGradeNames) {
fluid.each(newGradeNames, function (gradeName) {
if (!rec.seenGrades[gradeName]) {
if (fluid.isIoCReference(gradeName)) {
rec.rawDynamic.push(gradeName);
rec.seenGrades[gradeName] = true;
} else if (!fluid.contains(rec.oldGradeNames, gradeName)) {
rec.plainDynamic.push(gradeName);
}
}
});
};
fluid.computeDynamicGrades = function (that, shadow, strategy) {
delete that.options.gradeNames; // Recompute gradeNames for FLUID-5012 and others
var gradeNames = fluid.driveStrategy(that.options, "gradeNames", strategy); // Just acquire the reference and force eval of mergeBlocks "target", contents are wrong
gradeNames.length = 0;
// TODO: In complex distribution cases, a component might end up with multiple default blocks
var defaultsBlock = fluid.findMergeBlocks(shadow.mergeOptions.mergeBlocks, "defaults")[0];
var rec = {
that: that,
shadow: shadow,
defaultsBlock: defaultsBlock,
gradeNames: gradeNames, // remember that this array is globally shared
seenGrades: {},
plainDynamic: [],
rawDynamic: []
};
fluid.each(shadow.mergeOptions.mergeBlocks, function (block) { // acquire parents of earlier blocks before applying later ones
gradeNames.push.apply(gradeNames, fluid.makeArray(block.target && block.target.gradeNames));
fluid.applyDynamicGrades(rec);
});
fluid.collectDistributedGrades(rec);
while (true) {
while (rec.plainDynamic.length > 0) {
gradeNames.push.apply(gradeNames, rec.plainDynamic);
rec.plainDynamic.length = 0;
fluid.applyDynamicGrades(rec);
fluid.collectDistributedGrades(rec);
}
if (rec.rawDynamic.length > 0) {
var expanded = fluid.expandImmediate(rec.rawDynamic.shift(), that, shadow.localDynamic);
if (typeof(expanded) === "function") {
expanded = expanded();
}
if (expanded) {
rec.plainDynamic = rec.plainDynamic.concat(expanded);
}
} else {
break;
}
}
if (shadow.collectedClearer) {
shadow.collectedClearer();
delete shadow.collectedClearer;
}
};
fluid.computeDynamicComponentKey = function (recordKey, sourceKey) {
return recordKey + (sourceKey === 0 ? "" : "-" + sourceKey); // TODO: configurable name strategies
};
fluid.registerDynamicRecord = function (that, recordKey, sourceKey, record, toCensor) {
var key = fluid.computeDynamicComponentKey(recordKey, sourceKey);
var cRecord = fluid.copy(record);
delete cRecord[toCensor];
fluid.set(that.options, ["components", key], cRecord);
return key;
};
fluid.computeDynamicComponents = function (that, mergeOptions) {
var shadow = fluid.shadowForComponent(that);
var localSub = shadow.subcomponentLocal = {};
var records = fluid.driveStrategy(that.options, "dynamicComponents", mergeOptions.strategy);
fluid.each(records, function (record, recordKey) {
if (!record.sources && !record.createOnEvent) {
fluid.fail("Cannot process dynamicComponents record ", record, " without a \"sources\" or \"createOnEvent\" entry");
}
if (record.sources) {
var sources = fluid.expandOptions(record.sources, that);
fluid.each(sources, function (source, sourceKey) {
var key = fluid.registerDynamicRecord(that, recordKey, sourceKey, record, "sources");
localSub[key] = {"source": source, "sourcePath": sourceKey};
});
}
else if (record.createOnEvent) {
var event = fluid.event.expandOneEvent(that, record.createOnEvent);
fluid.set(shadow, ["dynamicComponentCount", recordKey], 0);
var listener = function () {
var key = fluid.registerDynamicRecord(that, recordKey, shadow.dynamicComponentCount[recordKey]++, record, "createOnEvent");
var localRecord = {"arguments": fluid.makeArray(arguments)};
fluid.initDependent(that, key, localRecord);
};
event.addListener(listener);
fluid.recordListener(event, listener, shadow);
}
});
};
// Second sequence point for mergeOptions from Fluid.js - here we construct all further
// strategies required on the IoC side and mount them into the shadow's getConfig for universal use
fluid.computeComponentAccessor = function (that, localRecord) {
var instantiator = fluid.globalInstantiator;
var shadow = fluid.shadowForComponent(that);
shadow.localDynamic = localRecord; // for signalling to dynamic grades from dynamic components
var options = that.options;
var strategy = shadow.mergeOptions.strategy;
var optionsStrategy = fluid.mountStrategy(["options"], options, strategy);
shadow.invokerStrategy = fluid.recordStrategy(that, options, strategy, "invokers", fluid.invokerFromRecord);
shadow.eventStrategyBlock = fluid.recordStrategy(that, options, strategy, "events", fluid.eventFromRecord, ["events"]);
var eventStrategy = fluid.mountStrategy(["events"], that, shadow.eventStrategyBlock.strategy, ["events"]);
shadow.memberStrategy = fluid.recordStrategy(that, options, strategy, "members", fluid.memberFromRecord, null, {model: true, modelRelay: true});
// NB - ginger strategy handles concrete, rationalise
shadow.getConfig = {strategies: [fluid.model.funcResolverStrategy, fluid.makeGingerStrategy(that),
optionsStrategy, shadow.invokerStrategy.strategy, shadow.memberStrategy.strategy, eventStrategy]};
fluid.computeDynamicGrades(that, shadow, strategy, shadow.mergeOptions.mergeBlocks);
fluid.distributeOptions(that, strategy);
if (shadow.contextHash["fluid.resolveRoot"]) {
var memberName;
if (shadow.contextHash["fluid.resolveRootSingle"]) {
var singleRootType = fluid.getForComponent(that, ["options", "singleRootType"]);
if (!singleRootType) {
fluid.fail("Cannot register object with grades " + Object.keys(shadow.contextHash).join(", ") + " as fluid.resolveRootSingle since it has not defined option singleRootType");
}
memberName = fluid.typeNameToMemberName(singleRootType);
} else {
memberName = fluid.computeGlobalMemberName(that);
}
var parent = fluid.resolveRootComponent;
if (parent[memberName]) {
instantiator.clearComponent(parent, memberName);
}
instantiator.recordKnownComponent(parent, that, memberName, false);
}
return shadow.getConfig;
};
// About the SHADOW:
// Allocated at: instantiator's "recordComponent"
// Contents:
// path {String} Principal allocated path (point of construction) in tree
// that {Component} The component itself
// contextHash {String to Boolean} Map of context names which this component matches
// mergePolicy, mergeOptions: Machinery for last phase of options merging
// invokerStrategy, eventStrategyBlock, memberStrategy, getConfig: Junk required to operate the accessor
// listeners: Listeners registered during this component's construction, to be cleared during clearListeners
// distributions, collectedClearer: Managing options distributions
// outDistributions: A list of distributions registered from this component, signalling from distributeOptions to clearDistributions
// subcomponentLocal: Signalling local record from computeDynamicComponents to assembleCreatorArguments
// dynamicLocal: Local signalling for dynamic grades
// ownScope: A hash of names to components which are in scope from this component - populated in cacheShadowGrades
// childrenScope: A hash of names to components which are in scope because they are children of this component (BELOW own ownScope in resolution order)
fluid.shadowForComponent = function (component) {
var instantiator = fluid.getInstantiator(component);
return instantiator && component ? instantiator.idToShadow[component.id] : null;
};
// Access the member at a particular path in a component, forcing it to be constructed gingerly if necessary
// supported, PUBLIC API function
fluid.getForComponent = function (component, path) {
var shadow = fluid.shadowForComponent(component);
var getConfig = shadow ? shadow.getConfig : undefined;
return fluid.get(component, path, getConfig);
};
// An EL segment resolver strategy that will attempt to trigger creation of
// components that it discovers along the EL path, if they have been defined but not yet
// constructed.
fluid.makeGingerStrategy = function (that) {
var instantiator = fluid.getInstantiator(that);
return function (component, thisSeg, index, segs) {
var atval = component[thisSeg];
if (atval === fluid.inEvaluationMarker && index === segs.length) {
fluid.fail("Error in component configuration - a circular reference was found during evaluation of path segment \"" + thisSeg +
"\": for more details, see the activity records following this message in the console, or issue fluid.setLogging(fluid.logLevel.TRACE) when running your application");
}
if (index > 1) {
return atval;
}
if (atval === undefined && component.hasOwnProperty(thisSeg)) { // avoid recomputing properties that have been explicitly evaluated to undefined
return fluid.NO_VALUE;
}
if (atval === undefined) { // pick up components in instantiation here - we can cut this branch by attaching early
var parentPath = instantiator.idToShadow[component.id].path;
var childPath = instantiator.composePath(parentPath, thisSeg);
atval = instantiator.pathToComponent[childPath];
}
if (atval === undefined) {
// TODO: This check is very expensive - once gingerness is stable, we ought to be able to
// eagerly compute and cache the value of options.components - check is also incorrect and will miss injections
var subRecord = fluid.getForComponent(component, ["options", "components", thisSeg]);
if (subRecord) {
if (subRecord.createOnEvent) {
fluid.fail("Error resolving path segment \"" + thisSeg + "\" of path " + segs.join(".") + " since component with record ", subRecord,
" has annotation \"createOnEvent\" - this very likely represents an implementation error. Either alter the reference so it does not " +
" match this component, or alter your workflow to ensure that the component is instantiated by the time this reference resolves");
}
fluid.initDependent(component, thisSeg);
atval = component[thisSeg];
}
}
return atval;
};
};
// Listed in dependence order
fluid.frameworkGrades = ["fluid.component", "fluid.modelComponent", "fluid.viewComponent", "fluid.rendererComponent"];
fluid.filterBuiltinGrades = function (gradeNames) {
return fluid.remove_if(fluid.makeArray(gradeNames), function (gradeName) {
return fluid.frameworkGrades.indexOf(gradeName) !== -1;
});
};
fluid.dumpGradeNames = function (that) {
return that.options && that.options.gradeNames ?
" gradeNames: " + JSON.stringify(fluid.filterBuiltinGrades(that.options.gradeNames)) : "";
};
fluid.dumpThat = function (that) {
return "{ typeName: \"" + that.typeName + "\"" + fluid.dumpGradeNames(that) + " id: " + that.id + "}";
};
fluid.dumpThatStack = function (thatStack, instantiator) {
var togo = fluid.transform(thatStack, function (that) {
var path = instantiator.idToPath(that.id);
return fluid.dumpThat(that) + (path ? (" - path: " + path) : "");
});
return togo.join("\n");
};
fluid.dumpComponentPath = function (that) {
var path = fluid.pathForComponent(that);
return path ? fluid.pathUtil.composeSegments(path) : "** no path registered for component **";
};
fluid.resolveContext = function (context, that, fast) {
if (context === "that") {
return that;
}
// TODO: Check performance impact of this type check introduced for FLUID-5903 in a very sensitive corner
if (typeof(context) === "object") {
var innerContext = fluid.resolveContext(context.context, that, fast);
if (!fluid.isComponent(innerContext)) {
fluid.triggerMismatchedPathError(context.context, that);
}
var rawValue = fluid.getForComponent(innerContext, context.path);
// TODO: Terrible, slow dispatch for this route
var expanded = fluid.expandOptions(rawValue, that);
if (!fluid.isComponent(expanded)) {
fluid.fail("Unable to resolve recursive context expression " + fluid.renderContextReference(context) + ": the directly resolved value of " + rawValue +
" did not resolve to a component in the scope of component ", that, ": got ", expanded);
}
return expanded;
} else {
var foundComponent;
var instantiator = fluid.globalInstantiator; // fluid.getInstantiator(that); // this hash lookup takes over 1us!
if (fast) {
var shadow = instantiator.idToShadow[that.id];
return shadow.ownScope[context];
} else {
var thatStack = instantiator.getFullStack(that);
fluid.visitComponentsForVisibility(instantiator, thatStack, function (component, name) {
var shadow = fluid.shadowForComponent(component);
// TODO: Some components, e.g. the static environment and typeTags do not have a shadow, which slows us down here
if (context === name || shadow && shadow.contextHash && shadow.contextHash[context] || context === component.typeName) {
foundComponent = component;
return true; // YOUR VISIT IS AT AN END!!
}
if (fluid.getForComponent(component, ["options", "components", context]) && !component[context]) {
// This is an expensive guess since we make it for every component up the stack - must apply the WAVE OF EXPLOSIONS (FLUID-4925) to discover all components first
// This line attempts a hopeful construction of components that could be guessed by nickname through finding them unconstructed
// in options. In the near future we should eagerly BEGIN the process of constructing components, discovering their
// types and then attaching them to the tree VERY EARLY so that we get consistent results from different strategies.
foundComponent = fluid.getForComponent(component, context);
return true;
}
});
return foundComponent;
}
}
};
fluid.triggerMismatchedPathError = function (parsed, parentThat) {
var ref = fluid.renderContextReference(parsed);
fluid.fail("Failed to resolve reference " + ref + " - could not match context with name " +
parsed.context + " from component " + fluid.dumpThat(parentThat) + " at path " + fluid.dumpComponentPath(parentThat) + " component: " , parentThat);
};
fluid.makeStackFetcher = function (parentThat, localRecord, fast) {
var fetcher = function (parsed) {
if (parentThat && parentThat.lifecycleStatus === "destroyed") {
fluid.fail("Cannot resolve reference " + fluid.renderContextReference(parsed) + " from component " + fluid.dumpThat(parentThat) + " which has been destroyed");
}
var context = parsed.context;
if (localRecord && context in localRecord) {
return fluid.get(localRecord[context], parsed.path);
}
var foundComponent = fluid.resolveContext(context, parentThat, fast);
if (!foundComponent && parsed.path !== "") {
fluid.triggerMismatchedPathError(parsed, parentThat);
}
return fluid.getForComponent(foundComponent, parsed.path);
};
return fetcher;
};
fluid.makeStackResolverOptions = function (parentThat, localRecord, fast) {
return $.extend(fluid.copy(fluid.rawDefaults("fluid.makeExpandOptions")), {
localRecord: localRecord || {},
fetcher: fluid.makeStackFetcher(parentThat, localRecord, fast),
contextThat: parentThat,
exceptions: {members: {model: true, modelRelay: true}}
});
};
fluid.clearListeners = function (shadow) {
// TODO: bug here - "afterDestroy" listeners will be unregistered already unless they come from this component
fluid.each(shadow.listeners, function (rec) {
rec.event.removeListener(rec.listenerId || rec.listener);
});
delete shadow.listeners;
};
fluid.recordListener = function (event, listener, shadow, listenerId) {
if (event.ownerId !== shadow.that.id) { // don't bother recording listeners registered from this component itself
fluid.pushArray(shadow, "listeners", {event: event, listener: listener, listenerId: listenerId});
}
};
fluid.constructScopeObjects = function (instantiator, parent, child, childShadow) {
var parentShadow = parent ? instantiator.idToShadow[parent.id] : null;
childShadow.childrenScope = parentShadow ? Object.create(parentShadow.ownScope) : {};
childShadow.ownScope = Object.create(childShadow.childrenScope);
childShadow.parentShadow = parentShadow;
};
fluid.clearChildrenScope = function (instantiator, parentShadow, child, childShadow) {
fluid.each(childShadow.contextHash, function (troo, context) {
if (parentShadow.childrenScope[context] === child) {
delete parentShadow.childrenScope[context]; // TODO: ambiguous resolution
}
});
};
// unsupported, non-API function - however, this structure is of considerable interest to those debugging
// into IoC issues. The structures idToShadow and pathToComponent contain a complete map of the component tree
// forming the surrounding scope
fluid.instantiator = function () {
var that = fluid.typeTag("instantiator");
$.extend(that, {
lifecycleStatus: "constructed",
pathToComponent: {},
idToShadow: {},
modelTransactions: {init: {}}, // a map of transaction id to map of component id to records of components enlisted in a current model initialisation transaction
composePath: fluid.model.composePath, // For speed, we declare that no component's name may contain a period
composeSegments: fluid.model.composeSegments,
parseEL: fluid.model.parseEL,
events: {
onComponentAttach: fluid.makeEventFirer({name: "instantiator's onComponentAttach event"}),
onComponentClear: fluid.makeEventFirer({name: "instantiator's onComponentClear event"})
}
});
// TODO: this API can shortly be removed
that.idToPath = function (id) {
var shadow = that.idToShadow[id];
return shadow ? shadow.path : "";
};
// Note - the returned stack is assumed writeable and does not include the root
that.getThatStack = function (component) {
var shadow = that.idToShadow[component.id];
if (shadow) {
var path = shadow.path;
var parsed = that.parseEL(path);
var root = that.pathToComponent[""], togo = [];
for (var i = 0; i < parsed.length; ++i) {
root = root[parsed[i]];
togo.push(root);
}
return togo;
}
else { return [];}
};
that.getFullStack = function (component) {
var thatStack = component ? that.getThatStack(component) : [];
thatStack.unshift(fluid.resolveRootComponent);
return thatStack;
};
function recordComponent(parent, component, path, name, created) {
var shadow;
if (created) {
shadow = that.idToShadow