Fixes #817 - Support dynamic root elements

[Optimizations] Simplified bookkeeping for component tree
A component stack is no longer used

Small API improvements

Improved how component boundaries are managed

Assign keys to all HTML elements and custom tags for better diffing

Checking in progress

Just build the src when calculating size
This commit is contained in:
Patrick Steele-Idem 2017-08-03 17:00:44 -06:00
parent 8f19884eae
commit 2be98636ea
460 changed files with 8362 additions and 5388 deletions

View File

@ -38,10 +38,10 @@ var minifiers = {
const out = gcc.compile(options);
// if (out.errors && out.errors.length) {
// console.error(out.errors);
// throw new Error(`Minification failed for ${file}`);
// }
if (out.errors && out.errors.length) {
console.error(out.errors);
throw new Error(`Minification failed for ${file}`);
}
return out.compiledCode;
},
uglify: function minifyUglifyJS(src, file) {
@ -139,4 +139,4 @@ promiseChain.then(() => {
}
console.log('Minification complete.');
});
});

View File

@ -11,7 +11,7 @@
"build-react": "npm run bundle-react --silent && node minify.js react",
"build-inferno": "npm run bundle-inferno --silent && node minify.js inferno",
"bundle": "mkdir -p build/bundles && npm run bundle-marko && npm run bundle-react && npm run bundle-vue && npm run bundle-preact && npm run bundle-inferno",
"bundle-marko": "node ../../scripts/build && NODE_ENV=production rollup -c marko/rollup.config.js",
"bundle-marko": "node ../../scripts/build src && NODE_ENV=production rollup -c marko/rollup.config.js",
"bundle-react": "NODE_ENV=production rollup -c react/rollup.config.js",
"bundle-preact": "NODE_ENV=production rollup -c preact/rollup.config.js",
"bundle-vue": "NODE_ENV=production rollup -c vue/rollup.config.js",

View File

@ -21,6 +21,7 @@
},
"scripts": {
"build": "node scripts/build.js",
"build-src": "node scripts/build.js src",
"prepublish": "npm run build",
"test": "npm run jshint -s && npm run mocha -s && npm run test-components -s && npm run test-components-deprecated -s",
"test-ci": "npm run test-generate-coverage && npm run build && NODE_ENV=production npm run test",

View File

@ -16,34 +16,44 @@ const babelOptions = {
]
};
buildDir('src', 'dist', {
babelExclude: [
'/taglibs/async/client-reorder-runtime.min.js'
],
babelOptions
});
var buildConstants = {
isDebug: false
};
var target = process.argv[2];
var shouldBuildSrc = true;
var shouldBuildTest = true;
if (target === 'src') {
shouldBuildTest = false;
}
if (shouldBuildSrc) {
buildDir('src', 'dist', {
babelExclude: [
'/taglibs/async/client-reorder-runtime.min.js'
],
babelOptions
});
}
fs.writeFileSync(
path.join(__dirname, '../dist/build.json'),
JSON.stringify({ isDebug: false }, null, 4),
{ encoding: 'utf8' });
buildDir('test', 'test-dist', {
babelExclude: [
'*expected*.*',
'input.js*'
],
exclude: [
'/generated',
'*.marko.js',
'*.skip',
'*.generated.*',
'*actual*.*',
'actualized-expected.html*'
],
babelOptions
});
if (shouldBuildTest) {
buildDir('test', 'test-dist', {
babelExclude: [
'*expected*.*',
'input.js*'
],
exclude: [
'/generated',
'*.marko.js',
'*.skip',
'*.generated.*',
'*actual*.*',
'actualized-expected.html*'
],
babelOptions
});
}

View File

@ -130,15 +130,22 @@ class Builder {
return new MemberExpression({object, property, computed});
}
/**
* Generates code that joins all of the arguments using `+` (BinaryExpression)
*
* @param {Array} args If the args object is not an array then `arguments` is used
* @return {Node} The resulting Node
*/
concat(args) {
var prev;
let operator = '+';
args = Array.isArray(args) ? args : Array.prototype.slice.call(arguments, 0);
for (var i=1; i<arguments.length; i++) {
for (var i=1; i<args.length; i++) {
var left;
var right = makeNode(arguments[i]);
var right = makeNode(args[i]);
if (i === 1) {
left = makeNode(arguments[i-1]);
left = makeNode(args[i-1]);
} else {
left = prev;
}
@ -153,15 +160,15 @@ class Builder {
return new ConditionalExpression({test, consequent, alternate});
}
containerNode(type, generateCode) {
containerNode(type, codeGenerator) {
if (typeof type === 'function') {
generateCode = arguments[0];
codeGenerator = arguments[0];
type = 'ContainerNode';
}
var node = new ContainerNode(type);
if (generateCode) {
node.setCodeGenerator(generateCode);
if (codeGenerator) {
node.setCodeGenerator(codeGenerator);
}
return node;
}

View File

@ -136,8 +136,8 @@ class CompileContext extends EventEmitter {
this.compilerType = this.options.compilerType || 'marko';
this.compilerVersion = this.options.compilerVersion || markoPkgVersion;
this.writeVersionComment = writeVersionComment !== 'undefined' ? writeVersionComment : true;
this.ignoreUnrecognizedTags = this.options.ignoreUnrecognizedTags || false;
this.escapeAtTags = this.options.escapeAtTags || false;
this.ignoreUnrecognizedTags = this.options.ignoreUnrecognizedTags === true;
this.escapeAtTags = this.options.escapeAtTags === true;
this._vars = {};
this._uniqueVars = new UniqueVars();
@ -168,6 +168,7 @@ class CompileContext extends EventEmitter {
this._imports = {};
this._fingerprint = undefined;
this._optimizers = undefined;
this.isComponent = false;
}
setInline(isInline) {

View File

@ -257,6 +257,14 @@ class Parser {
attrDef.argument = attr.argument.value;
}
var attrName = attr.name;
if (attrName) {
if (attrName === 'for-key' || attrName === 'for-ref' || attrName === 'w-for' || attrName.endsWith(':key')) {
context.data.hasLegacyForKey = true;
}
}
parsedAttributes.push(attrDef);
});
}

View File

@ -24,9 +24,9 @@ class DocumentType extends Node {
toJSON() {
return {
type: this.type,
value: this.value
value: this.documentType
};
}
}
module.exports = DocumentType;
module.exports = DocumentType;

View File

@ -23,18 +23,29 @@ class HtmlComment extends Node {
var comment = this.comment;
var builder = codegen.builder;
if (Array.isArray(comment)) {
comment = builder.concat(comment);
}
const commentArgs = [comment];
return builder.functionCall(
builder.memberExpression(
builder.identifierOut(),
builder.identifier('comment')),
[
comment
]);
commentArgs);
}
walk(walker) {
this.comment = walker.walk(this.comment);
}
setPropertyValue(name, value) {
if (!this._properties) {
this._properties = {};
}
this._properties[name] = value;
}
}
module.exports = HtmlComment;
module.exports = HtmlComment;

View File

@ -62,11 +62,21 @@ class HtmlElement extends Node {
this.selfClosed = def.selfClosed;
this.dynamicAttributes = undefined;
this.bodyOnlyIf = undefined;
this.runtimeFlags = 0; // Runtime flags are used to flag VDOM nodes with important information (flags are OR'd together)
this.key = undefined;
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
}
addRuntimeFlag(value) {
this.runtimeFlags |= value;
}
setKey(key) {
this.key = key;
}
generateHTMLCode(codegen) {
if (codegen.context.isMacro(this.tagName)) {
// At code generation time, if node tag corresponds to a registered macro

View File

@ -3,9 +3,21 @@
const Node = require('../../Node');
const vdomUtil = require('../../../util/vdom');
var FLAG_IS_SVG = 1;
var FLAG_IS_TEXTAREA = 2;
var FLAG_SIMPLE_ATTRS = 4;
const FLAG_IS_SVG = 1;
const FLAG_IS_TEXTAREA = 2;
const FLAG_SIMPLE_ATTRS = 4;
// const FLAG_PRESERVE = 8;
// const FLAG_COMPONENT_START_NODE = 16;
// const FLAG_COMPONENT_END_NODE = 32;
let CREATE_ARGS_COUNT = 0;
const INDEX_TAG_NAME = CREATE_ARGS_COUNT++;
const INDEX_ATTRS = CREATE_ARGS_COUNT++;
const INDEX_KEY = CREATE_ARGS_COUNT++;
const INDEX_COMPONENT = CREATE_ARGS_COUNT++;
const INDEX_CHILD_COUNT = CREATE_ARGS_COUNT++;
const INDEX_FLAGS = CREATE_ARGS_COUNT++;
const INDEX_PROPS = CREATE_ARGS_COUNT++;
function finalizeCreateArgs(createArgs, builder) {
var length = createArgs.length;
@ -17,7 +29,7 @@ function finalizeCreateArgs(createArgs, builder) {
lastArg = arg;
} else {
if (lastArg != null) {
if (i === 3) {
if (i === INDEX_FLAGS) {
// Use a literal 0 for the flags
createArgs[i] = builder.literal(0);
} else {
@ -46,6 +58,21 @@ const SIMPLE_ATTRS = {
'id': true
};
function isStaticProperties(properties) {
for (var k in properties) {
var v = properties[k];
if (v.type !== 'Literal') {
return false;
}
if (typeof v.value === 'object') {
return false;
}
}
return true;
}
class HtmlElementVDOM extends Node {
constructor(def) {
super('HtmlElementVDOM');
@ -57,6 +84,8 @@ class HtmlElementVDOM extends Node {
this.properties = def.properties;
this.body = def.body;
this.dynamicAttributes = def.dynamicAttributes;
this.key = def.key;
this.runtimeFlags = def.runtimeFlags;
this.isSVG = false;
this.isTextArea = false;
@ -69,7 +98,6 @@ class HtmlElementVDOM extends Node {
this.createElementId = undefined;
this.attributesArg = undefined;
this.propertiesArg = undefined;
this.nextConstId = undefined;
}
generateCode(codegen) {
@ -186,6 +214,8 @@ class HtmlElementVDOM extends Node {
this.attributesArg = attributesArg;
this.properties = builder.literal(this.properties || {});
return this;
}
@ -200,25 +230,34 @@ class HtmlElementVDOM extends Node {
let body = this.body;
let attributesArg = this.attributesArg;
let nextConstId = this.nextConstId;
let tagName = this.tagName;
let key = this.key;
let childCount = body && body.length;
let createArgs = new Array(5); // tagName, attributes, childCount, const ID, flags
let createArgs = new Array(CREATE_ARGS_COUNT);
createArgs[0] = tagName;
createArgs[INDEX_TAG_NAME] = tagName;
if (attributesArg) {
createArgs[1] = attributesArg;
createArgs[INDEX_ATTRS] = attributesArg;
}
if (key) {
createArgs[INDEX_KEY] = key;
if (!this.isStatic) {
createArgs[INDEX_COMPONENT] = builder.identifier('component');
}
}
if (childCount != null) {
createArgs[2] = builder.literal(childCount);
createArgs[INDEX_CHILD_COUNT] = builder.literal(childCount);
}
var flags = 0;
if (this.isSVG) {
@ -233,19 +272,21 @@ class HtmlElementVDOM extends Node {
flags |= FLAG_SIMPLE_ATTRS;
}
if (this.runtimeFlags) {
flags |= this.runtimeFlags;
}
if (flags) {
createArgs[3] = builder.literal(flags);
createArgs[INDEX_FLAGS] = builder.literal(flags);
}
if (nextConstId) {
if (!this.properties) {
this.properties = {};
}
this.properties.c = nextConstId;
}
let properties = this.properties;
if (this.properties) {
createArgs[4] = builder.literal(this.properties);
if (this.properties && properties.type === 'Literal' && Object.keys(properties.value).length === 0) {
properties = null;
}
if (properties) {
createArgs[INDEX_PROPS] = properties;
}
// Remove trailing undefined arguments and convert non-trailing
@ -290,6 +331,21 @@ class HtmlElementVDOM extends Node {
writer.decIndent();
}
}
setConstId(value) {
this.properties.value.i = value;
}
finalizeProperties(context) {
if (this.properties.type === 'Literal' && isStaticProperties(this.properties.value)) {
if (Object.keys(this.properties.value).length === 0) {
this.properties = null;
} else {
this.properties = context.addStaticVar('props', this.properties);
}
}
}
}
module.exports = HtmlElementVDOM;

View File

@ -38,11 +38,16 @@ module.exports = function(node, codegen, vdomUtil) {
var attributes = codegen.generateCode(node.getAttributes());
var properties = codegen.generateCode(node.getProperties());
var dynamicAttributes = codegen.generateCode(node.dynamicAttributes);
var key = node.key;
var runtimeFlags = node.runtimeFlags;
var nextConstId = node.nextConstId;
var builder = codegen.builder;
var isKeyStatic = vdomUtil.isStaticValue(key);
var isAttrsStatic = checkAttributesStatic(attributes);
var isPropsStatic = checkPropertiesStatic(properties, vdomUtil);
var isStatic = isAttrsStatic && isPropsStatic && node.isLiteralTagName();
var isStatic = isKeyStatic && isAttrsStatic && isPropsStatic && node.isLiteralTagName();
var isHtmlOnly = true;
if (body && body.length) {
@ -71,6 +76,7 @@ module.exports = function(node, codegen, vdomUtil) {
}
var htmlElVDOM = new HtmlElementVDOM({
key,
tagName,
attributes,
properties,
@ -78,7 +84,9 @@ module.exports = function(node, codegen, vdomUtil) {
isStatic,
isAttrsStatic,
isHtmlOnly,
dynamicAttributes
dynamicAttributes,
nextConstId,
runtimeFlags
});

View File

@ -125,6 +125,13 @@ class Node {
this.body.appendChild(node);
}
appendChildren(nodes) {
ok(this.body, 'Node does not support child nodes: ' + this);
nodes.forEach((node) => {
this.body.appendChild(node);
});
}
insertBefore(newNode, referenceNode) {
ok(this.body, 'Node does not support child nodes: ' + this);
this.body.insertBefore(newNode, referenceNode);

View File

@ -53,19 +53,23 @@ class OptimizerContext {
}
class NodeVDOM extends Node {
constructor(variableIdentifier) {
constructor(variableIdentifier, isComponent) {
super('NodeVDOM');
this.variableIdentifier = variableIdentifier;
this.isComponent = isComponent;
}
writeCode(writer) {
var builder = writer.builder;
var funcCallArgs = [ this.variableIdentifier ];
if (this.isComponent) {
funcCallArgs.push(builder.identifier('component'));
}
let funcCall = builder.functionCall(
builder.identifier('n'),
[
this.variableIdentifier
]);
funcCallArgs);
if (this.isChild) {
writer.write('.');
@ -89,16 +93,16 @@ function generateNodesForArray(nodes, context, options) {
function generateStaticNode(node) {
if (node.type === 'HtmlElementVDOM') {
node.createElementId = context.helper('createElement');
node.setConstId(builder.functionCall(optimizerContext.nextConstIdFunc, []));
}/* else {
node.createTextId = context.importModule('marko_createText', 'marko/vdom/createText');
}*/
node.nextConstId = builder.functionCall(optimizerContext.nextConstIdFunc, []);
node.isStaticRoot = true;
let staticNodeId = context.addStaticVar('marko_node' + (optimizerContext.nextNodeId++), node);
return new NodeVDOM(staticNodeId);
return new NodeVDOM(staticNodeId, context.isComponent);
}
function handleStaticAttributes(node) {
@ -131,6 +135,7 @@ function generateNodesForArray(nodes, context, options) {
finalNodes.push(node);
}
node.finalizeProperties(context);
} else {
finalNodes.push(node);
}
@ -157,4 +162,4 @@ class VDOMOptimizer {
}
}
module.exports = VDOMOptimizer;
module.exports = VDOMOptimizer;

View File

@ -7,53 +7,27 @@ var getComponentsContext = require('./ComponentsContext').___getComponentsContex
var componentsUtil = require('./util');
var componentLookup = componentsUtil.___componentLookup;
var emitLifecycleEvent = componentsUtil.___emitLifecycleEvent;
var destroyComponentForEl = componentsUtil.___destroyComponentForEl;
var destroyElRecursive = componentsUtil.___destroyElRecursive;
var getElementById = componentsUtil.___getElementById;
var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
var EventEmitter = require('events-light');
var RenderResult = require('../runtime/RenderResult');
var SubscriptionTracker = require('listener-tracker');
var inherit = require('raptor-util/inherit');
var updateManager = require('./update-manager');
var morphdom = require('../morphdom');
var eventDelegation = require('./event-delegation');
var slice = Array.prototype.slice;
var MORPHDOM_SKIP = true;
var COMPONENT_SUBSCRIBE_TO_OPTIONS;
var NON_COMPONENT_SUBSCRIBE_TO_OPTIONS = {
addDestroyListener: false
};
function outNoop() { /* jshint -W040 */ return this; }
var emit = EventEmitter.prototype.emit;
function removeListener(removeEventListenerHandle) {
removeEventListenerHandle();
}
function checkCompatibleComponent(globalComponentsContext, el) {
var component = el._w;
while(component) {
var id = component.id;
var newComponentDef = globalComponentsContext.___componentsById[id];
if (newComponentDef && component.___type == newComponentDef.___component.___type) {
break;
}
var rootFor = component.___rootFor;
if (rootFor) {
component = rootFor;
} else {
component.___destroyShallow();
break;
}
}
}
function handleCustomEventWithMethodListener(component, targetMethodName, args, extraArgs) {
// Remove the "eventType" argument
args.push(component);
@ -72,16 +46,12 @@ function handleCustomEventWithMethodListener(component, targetMethodName, args,
targetMethod.apply(targetComponent, args);
}
function getElIdHelper(component, componentElId, index) {
var id = component.id;
function resolveKeyHelper(key, index) {
return index ? key + '_' + index : key;
}
var elId = componentElId != null ? id + '-' + componentElId : id;
if (index != null) {
elId += '[' + index + ']';
}
return elId;
function resolveComponentIdHelper(component, key, index) {
return component.id + '-' + resolveKeyHelper(key, index);
}
/**
@ -158,46 +128,10 @@ function checkInputChanged(existingComponent, oldInput, newInput) {
return false;
}
function onNodeDiscarded(node) {
if (node.nodeType === 1) {
destroyComponentForEl(node);
}
}
function onBeforeNodeDiscarded(node) {
return eventDelegation.___handleNodeDetach(node);
}
function onBeforeElUpdated(fromEl, key, globalComponentsContext) {
if (key) {
var preserved = globalComponentsContext.___preserved[key];
if (preserved === true) {
// Don't morph elements that are associated with components that are being
// reused or elements that are being preserved. For components being reused,
// the morphing will take place when the reused component updates.
return MORPHDOM_SKIP;
} else {
// We may need to destroy a Component associated with the current element
// if a new UI component was rendered to the same element and the types
// do not match
checkCompatibleComponent(globalComponentsContext, fromEl);
}
}
}
function onBeforeElChildrenUpdated(el, key, globalComponentsContext) {
if (key) {
var preserved = globalComponentsContext.___preservedBodies[key];
if (preserved === true) {
// Don't morph the children since they are preserved
return MORPHDOM_SKIP;
}
}
}
function onNodeAdded(node, globalComponentsContext) {
eventDelegation.___handleNodeAttach(node, globalComponentsContext.___out);
function getNodes(component) {
var nodes = [];
component.___forEachNode(nodes.push.bind(nodes));
return nodes;
}
var componentProto;
@ -210,9 +144,9 @@ var componentProto;
function Component(id) {
EventEmitter.call(this);
this.id = id;
this.el = null;
this.___state = null;
this.___roots = null;
this.___startNode = null;
this.___endNode = null;
this.___subscriptions = null;
this.___domEventListenerHandles = null;
this.___bubblingDomEvents = null; // Used to keep track of bubbling DOM events for components rendered on the server
@ -229,6 +163,8 @@ function Component(id) {
this.___settingInput = false;
this.___document = undefined;
this.___keyedElements = {};
}
Component.prototype = componentProto = {
@ -264,63 +200,57 @@ Component.prototype = componentProto = {
return emit.apply(this, arguments);
}
},
getElId: function (componentElId, index) {
return getElIdHelper(this, componentElId, index);
getElId: function (key, index) {
return resolveComponentIdHelper(this, key, index);
},
getEl: function (componentElId, index) {
var doc = this.___document;
if (componentElId != null) {
return getElementById(doc, getElIdHelper(this, componentElId, index));
getEl: function (key, index) {
if (key) {
return this.___keyedElements[resolveKeyHelper(key, index)];
} else {
return this.el || getElementById(doc, getElIdHelper(this));
return this.___startNode;
}
},
getEls: function(id) {
getEls: function(key) {
key = key + '[]';
var els = [];
var i = 0;
var el;
while((el = this.getEl(id, i))) {
while((el = this.getEl(key, i))) {
els.push(el);
i++;
}
return els;
},
getComponent: function(id, index) {
return componentLookup[getElIdHelper(this, id, index)];
getComponent: function(key, index) {
return componentLookup[resolveComponentIdHelper(this, key, index)];
},
getComponents: function(id) {
getComponents: function(key) {
key = key + '[]';
var components = [];
var i = 0;
var component;
while((component = componentLookup[getElIdHelper(this, id, i)])) {
while((component = componentLookup[resolveComponentIdHelper(this, key, i)])) {
components.push(component);
i++;
}
return components;
},
destroy: function() {
destroy: function(onBeforeNodeDiscarded) {
if (this.___destroyed) {
return;
}
var els = this.els;
var nodes = getNodes(this);
this.___destroyShallow();
var rootComponents = this.___rootComponents;
if (rootComponents) {
rootComponents.forEach(function(rootComponent) {
rootComponent.___destroy();
});
}
nodes.forEach(function(node) {
destroyNodeRecursive(node);
els.forEach(function(el) {
destroyElRecursive(el);
var parentNode = el.parentNode;
if (parentNode) {
parentNode.removeChild(el);
if (!onBeforeNodeDiscarded || onBeforeNodeDiscarded(node) != false) {
node.parentNode.removeChild(node);
}
});
},
@ -333,7 +263,9 @@ Component.prototype = componentProto = {
emitLifecycleEvent(this, 'destroy');
this.___destroyed = true;
this.el = null;
this.___startNode.___markoComponent = undefined;
this.___startNode = this.___endNode = null;
// Unsubscribe from all DOM events
this.___removeDOMEventListeners();
@ -452,6 +384,7 @@ Component.prototype = componentProto = {
___queueUpdate: function() {
if (!this.___updateQueued) {
this.___updateQueued = true;
updateManager.___queueComponentUpdate(this);
}
},
@ -512,7 +445,10 @@ Component.prototype = componentProto = {
if (!renderer) {
throw TypeError();
}
var fromEls = self.___getRootEls({});
var startNode = this.___startNode;
var endNodeNextSibling = this.___endNode.nextSibling;
var doc = self.___document;
var input = this.___renderInput || this.___input;
var globalData = this.___global;
@ -523,18 +459,6 @@ Component.prototype = componentProto = {
out.sync();
out.___document = self.___document;
if (isRerenderInBrowser === true) {
out.e =
out.be =
out.ee =
out.t =
out.h =
out.w =
out.write =
out.html =
outNoop;
}
var componentsContext = getComponentsContext(out);
var globalComponentsContext = componentsContext.___globalContext;
globalComponentsContext.___rerenderComponent = self;
@ -544,68 +468,40 @@ Component.prototype = componentProto = {
var result = new RenderResult(out);
if (isRerenderInBrowser !== true) {
var targetNode = out.___getOutput();
var targetNode = out.___getOutput();
var fromEl;
var targetEl = targetNode.___firstChild;
while (targetEl) {
var nodeName = targetEl.___nodeName;
if (nodeName === 'HTML') {
fromEl = document.documentElement;
} else if (nodeName === 'BODY') {
fromEl = document.body;
} else if (nodeName === 'HEAD') {
fromEl = document.head;
} else {
fromEl = fromEls[targetEl.id];
}
if (fromEl) {
morphdom(
fromEl,
targetEl,
globalComponentsContext,
onNodeAdded,
onBeforeElUpdated,
onBeforeNodeDiscarded,
onNodeDiscarded,
onBeforeElChildrenUpdated);
}
targetEl = targetEl.___nextSibling;
}
}
morphdom(
startNode.parentNode,
startNode,
endNodeNextSibling,
targetNode,
doc,
componentsContext);
result.afterInsert(doc);
out.emit('___componentsInitialized');
});
this.___reset();
},
___getRootEls: function(rootEls) {
var i, len;
___detach: function() {
var fragment = this.___document.createDocumentFragment();
this.___forEachNode(fragment.appendChild.bind(fragment));
return fragment;
},
var componentEls = this.els;
___forEachNode: function(callback) {
var currentNode = this.___startNode;
var endNode = this.___endNode;
for (i=0, len=componentEls.length; i<len; i++) {
var componentEl = componentEls[i];
rootEls[componentEl.id] = componentEl;
}
var rootComponents = this.___rootComponents;
if (rootComponents) {
for (i=0, len=rootComponents.length; i<len; i++) {
var rootComponent = rootComponents[i];
rootComponent.___getRootEls(rootEls);
for(;;) {
var nextSibling = currentNode.nextSibling;
callback(currentNode);
if (currentNode == endNode) {
break;
}
currentNode = nextSibling;
}
return rootEls;
},
___removeDOMEventListeners: function() {
@ -632,6 +528,14 @@ Component.prototype = componentProto = {
finalCustomEvents[eventType] = [targetMethodName, extraArgs];
});
},
get el() {
return this.___startNode;
},
get els() {
return getNodes(this);
}
};
@ -649,17 +553,7 @@ componentProto.___destroy = componentProto.destroy;
domInsert(
componentProto,
function getEl(component) {
var els = this.els;
var elCount = els.length;
if (elCount > 1) {
var fragment = component.___document.createDocumentFragment();
els.forEach(function(el) {
fragment.appendChild(el);
});
return fragment;
} else {
return els[0];
}
return component.___detach();
},
function afterInsert(component) {
return component;

View File

@ -3,48 +3,50 @@ var repeatedRegExp = /\[\]$/;
var componentUtil = require('./util');
var attachBubblingEvent = componentUtil.___attachBubblingEvent;
var extend = require('raptor-util/extend');
var KeySequence = require('./KeySequence');
/*
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
var FLAG_HAS_BODY_EL = 2;
var FLAG_HAS_HEAD_EL = 4;
*/
/**
* A ComponentDef is used to hold the metadata collected at runtime for
* a single component and this information is used to instantiate the component
* later (after the rendered HTML has been added to the DOM)
*/
function ComponentDef(component, componentId, globalComponentsContext, componentStack, componentStackLen) {
function ComponentDef(component, componentId, globalComponentsContext) {
this.___globalComponentsContext = globalComponentsContext; // The AsyncWriter that this component is associated with
this.___componentStack = componentStack;
this.___componentStackLen = componentStackLen;
this.___component = component;
this.id = componentId;
this.___roots = null; // IDs of root elements if there are multiple root elements
this.___children = null; // An array of nested ComponentDef instances
this.___domEvents = undefined; // An array of DOM events that need to be added (in sets of three)
this.___isExisting = false;
this.___willRerenderInBrowser = false;
this.___renderBoundary = false;
this.___flags = 0;
this.___nextIdIndex = 0; // The unique integer to use for the next scoped ID
this.___keySequence = null;
this.___preservedDOMNodes = null;
}
ComponentDef.prototype = {
___end: function() {
this.___componentStack.length = this.___componentStackLen;
___nextKey: function(key) {
var keySequence = this.___keySequence || (this.___keySequence = new KeySequence());
return keySequence.___nextKey(key);
},
/**
* Register a nested component for this component. We maintain a tree of components
* so that we can instantiate nested components before their parents.
*/
___addChild: function (componentDef) {
var children = this.___children;
if (children) {
children.push(componentDef);
} else {
this.___children = [componentDef];
}
___preserveDOMNode: function(key, bodyOnly) {
var lookup = this.___preservedDOMNodes || (this.___preservedDOMNodes = {});
lookup[key] = bodyOnly ? 2 : 1;
},
/**
* This helper method generates a unique and fully qualified DOM element ID
* that is unique within the scope of the current component. This method prefixes
@ -88,15 +90,15 @@ ComponentDef.prototype = {
* Returns the next auto generated unique ID for a nested DOM element or nested DOM component
*/
___nextComponentId: function() {
var id = this.id;
return id === null ?
this.___globalComponentsContext.___nextComponentId(this.___out) :
id + '-c' + (this.___nextIdIndex++);
return this.id + '-c' + (this.___nextIdIndex++);
},
d: function(handlerMethodName, extraArgs) {
return attachBubblingEvent(this, handlerMethodName, extraArgs);
},
get ___type() {
return this.___component.___type;
}
};
@ -146,10 +148,11 @@ ComponentDef.___deserialize = function(o, types, globals, registry) {
component.___global = globals;
return {
id: id,
___component: component,
___roots: extra.r,
___boundary: extra.r,
___domEvents: extra.d,
___willRerenderInBrowser: extra._ === 1
___flags: extra.f || 0
};
};

View File

@ -1,117 +1,47 @@
'use strict';
var GlobalComponentsContext = require('./GlobalComponentsContext');
var ComponentDef = require('./ComponentDef');
var componentsUtil = require('./util');
function ComponentsContext(out, parentComponentsContext) {
var globalComponentsContext;
var componentDef;
var components;
var beginComponent = require('./beginComponent');
if (parentComponentsContext) {
components = parentComponentsContext.___components;
globalComponentsContext = parentComponentsContext.___globalContext;
componentDef = parentComponentsContext.___componentDef;
} else {
globalComponentsContext = out.global.___components;
if (globalComponentsContext === undefined) {
out.global.___components = globalComponentsContext = new GlobalComponentsContext(out);
}
components = [];
}
var EMPTY_OBJECT = {};
function GlobalComponentsContext(out) {
this.___roots = [];
this.___preserved = EMPTY_OBJECT;
this.___preservedBodies = EMPTY_OBJECT;
this.___componentsById = {};
this.___globalContext = globalComponentsContext;
this.___components = components;
this.___out = out;
this.___rerenderComponent = undefined;
this.___nextIdLookup = null;
this.___nextComponentId = componentsUtil.___nextComponentIdProvider(out);
this.___componentDef = componentDef;
}
GlobalComponentsContext.prototype = {
ComponentsContext.prototype = {
___initComponents: function(doc) {
var topLevelComponentDefs = null;
var componentDefs = this.___components;
this.___roots.forEach(function(root) {
var children = root.___children;
if (children) {
// NOTE: ComponentsContext.___initClientRendered is provided by
// index-browser.js to avoid a circular dependency
ComponentsContext.___initClientRendered(children, doc);
if (topLevelComponentDefs === null) {
topLevelComponentDefs = children;
} else {
topLevelComponentDefs = topLevelComponentDefs.concat(children);
}
}
});
ComponentsContext.___initClientRendered(componentDefs, doc);
this.___roots = null;
this.___out.emit('___componentsInitialized');
// Reset things stored in global since global is retained for
// future renders
this.___out.global.___components = undefined;
return topLevelComponentDefs;
return componentDefs;
},
___preserveDOMNode: function(elId, bodyOnly) {
var preserved = bodyOnly === true ? this.___preservedBodies : this.___preserved;
if (preserved === EMPTY_OBJECT) {
if (bodyOnly === true) {
preserved = this.___preservedBodies = {};
} else {
preserved = this.___preserved = {};
}
}
preserved[elId] = true;
},
___nextRepeatedId: function(parentId, id) {
var nextIdLookup = this.___nextIdLookup || (this.___nextIdLookup = {});
var indexLookupKey = parentId + '-' + id;
var currentIndex = nextIdLookup[indexLookupKey];
if (currentIndex == null) {
currentIndex = nextIdLookup[indexLookupKey] = 0;
} else {
currentIndex = ++nextIdLookup[indexLookupKey];
}
return indexLookupKey.slice(0, -2) + '[' + currentIndex + ']';
}
};
function ComponentsContext(out, parentComponentsContext, shouldAddGlobalRoot) {
var root;
var globalComponentsContext;
if (parentComponentsContext === undefined) {
globalComponentsContext = out.global.___components;
if (globalComponentsContext === undefined) {
out.global.___components = globalComponentsContext = new GlobalComponentsContext(out);
}
root = new ComponentDef(null, null, globalComponentsContext);
if (shouldAddGlobalRoot !== false) {
globalComponentsContext.___roots.push(root);
}
} else {
globalComponentsContext = parentComponentsContext.___globalContext;
var parentComponentStack = parentComponentsContext.___componentStack;
root = parentComponentStack[parentComponentStack.length-1];
}
this.___globalContext = globalComponentsContext;
this.___out = out;
this.___componentStack = [root];
}
ComponentsContext.prototype = {
___createNestedComponentsContext: function(nestedOut) {
return new ComponentsContext(nestedOut, this);
},
___beginComponent: beginComponent,
___nextComponentId: function() {
var componentStack = this.___componentStack;
var parentComponentDef = componentStack[componentStack.length - 1];
return parentComponentDef.___nextComponentId();
}
};
function getComponentsContext(out) {
return out.data.___components || (out.data.___components = new ComponentsContext(out));
return out.___components || (out.___components = new ComponentsContext(out));
}
module.exports = exports = ComponentsContext;

View File

@ -0,0 +1,18 @@
var nextComponentIdProvider = require('./util').___nextComponentIdProvider;
var KeySequence = require('./KeySequence');
function GlobalComponentsContext(out) {
this.___preservedEls = {};
this.___preservedElBodies = {};
this.___renderedComponentsById = {};
this.___rerenderComponent = undefined;
this.___nextComponentId = nextComponentIdProvider(out);
}
GlobalComponentsContext.prototype = {
___createKeySequence: function() {
return new KeySequence();
}
};
module.exports = GlobalComponentsContext;

View File

@ -0,0 +1,27 @@
function KeySequence() {
this.___lookup = {};
}
KeySequence.prototype = {
___nextKey: function(key) {
// var len = key.length;
// var lastChar = key[len-1];
// if (lastChar === ']') {
// key = key.substring(0, len-2);
// }
var lookup = this.___lookup;
var currentIndex = lookup[key]++;
if (!currentIndex) {
lookup[key] = 1;
currentIndex = 0;
return key;
} else {
return key + '_' + currentIndex;
}
}
};
module.exports = KeySequence;

View File

@ -2,14 +2,15 @@ var eventDelegation = require('./event-delegation');
var delegateEvent = eventDelegation.___delegateEvent;
var getEventFromEl = eventDelegation.___getEventFromEl;
var componentsUtil = require('./util');
var destroyElRecursive = componentsUtil.___destroyElRecursive;
var destroyComponentForEl = componentsUtil.___destroyComponentForEl;
// var componentsUtil = require('./util');
// var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
// var destroyComponentForNode = componentsUtil.___destroyComponentForNode;
function handleNodeAttach(node, out) {
function handleNodeAttach(node, componentsContext) {
if (node.nodeType === 1) {
var target = getEventFromEl(node, 'onattach');
if (target) {
var out = componentsContext.___out;
var data = out.data;
var attachTargets = data.___attachTargets;
@ -39,8 +40,6 @@ function handleNodeDetach(node) {
delegateEvent(node, target, {
preventDefault: function() {
allowDetach = false;
destroyComponentForEl(node);
destroyElRecursive(node);
},
detach: function() {
var parentNode = node.parentNode;

View File

@ -1,17 +1,14 @@
var ComponentDef = require('./ComponentDef');
module.exports = function(component, isSplitComponent) {
var componentStack = this.___componentStack;
var origLength = componentStack.length;
var parentComponentDef = componentStack[origLength - 1];
module.exports = function beginComponent(componentsContext, component, isSplitComponen, parentComponentDeft) {
var componentId = component.id;
var componentDef = new ComponentDef(component, componentId, this.___globalContext, componentStack, origLength);
parentComponentDef.___addChild(componentDef);
this.___globalContext.___componentsById[componentId] = componentDef;
componentStack.push(componentDef);
var globalContext = componentsContext.___globalContext;
var componentDef = componentsContext.___componentDef = new ComponentDef(component, componentId, globalContext);
globalContext.___renderedComponentsById[componentId] = true;
componentsContext.___components.push(componentDef);
var out = componentsContext.___out;
out.bc(component);
return componentDef;
};

View File

@ -3,8 +3,13 @@
const ComponentDef = require('./ComponentDef');
const hasRenderBodyKey = Symbol.for("hasRenderBody");
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
// var FLAG_HAS_BODY_EL = 2;
// var FLAG_HAS_HEAD_EL = 4;
function isInputSerializable(component) {
var input = component.___input;
if (!input) {
return true;
}
@ -16,29 +21,33 @@ function isInputSerializable(component) {
}
}
module.exports = function beginComponent(component, isSplitComponent) {
var componentStack = this.___componentStack;
var origLength = componentStack.length;
var parentComponentDef = componentStack[origLength - 1];
module.exports = function beginComponent(componentsContext, component, isSplitComponent, parentComponentDef) {
var globalContext = componentsContext.___globalContext;
var componentId = component.id;
var componentDef = new ComponentDef(component, componentId, this.___globalContext, componentStack, origLength);
var componentDef = componentsContext.___componentDef = new ComponentDef(component, componentId, globalContext);
// On the server
if (parentComponentDef.___willRerenderInBrowser === true) {
componentDef.___willRerenderInBrowser = true;
} else {
parentComponentDef.___addChild(componentDef);
if (isSplitComponent === false &&
this.___out.global.noBrowserRerender !== true &&
isInputSerializable(component)) {
componentDef.___willRerenderInBrowser = true;
}
if (parentComponentDef && (parentComponentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER)) {
componentDef.___flags |= FLAG_WILL_RERENDER_IN_BROWSER;
return componentDef;
}
componentStack.push(componentDef);
componentsContext.___components.push(componentDef);
let out = componentsContext.___out;
componentDef.___renderBoundary = true;
if (isSplitComponent === false &&
out.global.noBrowserRerender !== true &&
isInputSerializable(component)) {
componentDef.___flags |= FLAG_WILL_RERENDER_IN_BROWSER;
out.w('<!--M#' + componentId + '-->');
} else {
out.w('<!--M^' + componentId + '-->');
}
return componentDef;
};

View File

@ -0,0 +1,5 @@
'use strict';
module.exports = function endComponent(out) {
out.ee(); // endElement() (also works for VComponent nodes pushed on to the stack)
};

View File

@ -0,0 +1,7 @@
'use strict';
module.exports = function endComponent(out, componentDef) {
if (componentDef.___renderBoundary) {
out.w('<!--M$' + componentDef.id + '-->');
}
};

View File

@ -3,12 +3,34 @@
var warp10 = require('warp10');
var escapeEndingScriptTagRegExp = /<\//g;
function flattenHelper(components, flattened, typesArray, typesLookup) {
for (var i = 0, len = components.length; i < len; i++) {
function getRenderedComponents(out, shouldIncludeAll) {
var componentsContext = out.___components;
if (componentsContext === null) {
return;
}
// console.log('componentsContext:', componentsContext);
var components = componentsContext.___components;
if (components.length === 0) {
return;
}
// console.log('components:', components.map((componentDef) => {
// return { id: componentDef.id, type: componentDef.type};
// }));
var componentsFinal = [];
var typesLookup = {};
var typesArray = [];
for (var i = components.length - 1; i >= 0; i--) {
var componentDef = components[i];
var id = componentDef.id;
var component = componentDef.___component;
var rerenderInBrowser = componentDef.___willRerenderInBrowser;
var flags = componentDef.___flags;
var state = component.state;
var input = component.input;
var typeName = component.typeName;
@ -36,14 +58,6 @@ function flattenHelper(components, flattened, typesArray, typesLookup) {
typesLookup[typeName] = typeIndex;
}
var children = componentDef.___children;
if (children !== null) {
// Depth-first search (children should be initialized before parent)
flattenHelper(children, flattened, typesArray, typesLookup);
componentDef.___children = null;
}
var hasProps = false;
let componentKeys = Object.keys(component);
@ -81,71 +95,23 @@ function flattenHelper(components, flattened, typesArray, typesLookup) {
b: bubblingDomEvents,
d: componentDef.___domEvents,
e: customEvents,
f: flags ? flags : undefined,
p: customEvents && scope, // Only serialize scope if we need to attach custom events
r: componentDef.___roots,
r: componentDef.___boundary,
s: state,
u: undefinedPropNames,
w: hasProps ? component : undefined,
_: rerenderInBrowser ? 1 : undefined
w: hasProps ? component : undefined
};
flattened.push([
componentsFinal.push([
id, // 0 = id
typeIndex, // 1 = type
input, // 2 = input
extra // 3
]);
}
}
function getRenderedComponents(out, shouldIncludeAll) {
var componentDefs;
var globalComponentsContext;
var outGlobal = out.global;
if (shouldIncludeAll === true) {
globalComponentsContext = outGlobal.___components;
if (globalComponentsContext === undefined) {
return undefined;
}
} else {
let componentsContext = out.data.___components;
if (componentsContext === undefined) {
return undefined;
}
let rootComponentDef = componentsContext.___componentStack[0];
componentDefs = rootComponentDef.___children;
if (componentDefs === null) {
return undefined;
}
rootComponentDef.___children = null;
}
var flattened = [];
var typesLookup = {};
var typesArray = [];
if (shouldIncludeAll === true) {
let roots = globalComponentsContext.___roots;
for (let i=0, len=roots.length; i<len; i++) {
let root = roots[i];
let children = root.___children;
if (children !== null) {
flattenHelper(children, flattened, typesArray, typesLookup);
}
}
} else {
flattenHelper(componentDefs, flattened, typesArray, typesLookup);
}
if (flattened.length === 0) {
return undefined;
}
return {w: flattened, t: typesArray};
return {w: componentsFinal, t: typesArray};
}
function writeInitComponentsCode(out, shouldIncludeAll) {

View File

@ -5,10 +5,56 @@ var win = window;
var defaultDocument = document;
var componentsUtil = require('./util');
var componentLookup = componentsUtil.___componentLookup;
var getElementById = componentsUtil.___getElementById;
var ComponentDef = require('./ComponentDef');
var registry = require('./registry');
var serverRenderedGlobals = {};
var serverComponentStartNodes = {};
var serverComponentEndNodes = {};
var keyedElementsByComponentId = {};
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
var FLAG_HAS_BODY_EL = 2;
var FLAG_HAS_HEAD_EL = 4;
function indexServerComponentBoundaries(node) {
var componentId;
node = node.firstChild;
while(node) {
if (node.nodeType === 8) { // Comment node
var commentValue = node.nodeValue;
if (commentValue[0] === 'M') {
componentId = commentValue.substring(2);
switch(commentValue[1]) {
case '$':
serverComponentEndNodes[componentId] = node;
break;
case '#': // This component will rerender in the browser, no need to index children
serverComponentStartNodes[componentId] = node;
var endValue = 'M$' + componentId;
while((node = node.nextSibling) && node.nodeValue !== endValue) {}
continue;
case '^':
serverComponentStartNodes[componentId] = node;
break;
}
}
} else if (node.nodeType === 1) { // HTML element node
var markoKey = node.getAttribute('data-marko-key');
if (markoKey) {
var separatorIndex = markoKey.indexOf(' ');
componentId = markoKey.substring(separatorIndex+1);
markoKey = markoKey.substring(0, separatorIndex);
var keyedElements = keyedElementsByComponentId[componentId] || (keyedElementsByComponentId[componentId] = {});
keyedElements[markoKey] = node;
}
indexServerComponentBoundaries(node);
}
node = node.nextSibling;
}
}
function invokeComponentEventHandler(component, targetMethodName, args) {
var method = component[targetMethodName];
@ -51,44 +97,9 @@ function initComponent(componentDef, doc) {
var isExisting = componentDef.___isExisting;
var id = component.id;
var rootIds = componentDef.___roots;
componentLookup[id] = component;
if (rootIds) {
var rootComponents;
var els = [];
rootIds.forEach(function(rootId) {
var nestedId = id + '-' + rootId;
var rootComponent = componentLookup[nestedId];
if (rootComponent) {
rootComponent.___rootFor = component;
if (rootComponents) {
rootComponents.push(rootComponent);
} else {
rootComponents = component.___rootComponents = [rootComponent];
}
} else {
var rootEl = getElementById(doc, nestedId);
if (rootEl) {
rootEl._w = component;
els.push(rootEl);
}
}
});
component.el = els[0];
component.els = els;
componentLookup[id] = component;
} else if (!isExisting) {
var el = getElementById(doc, id);
el._w = component;
component.el = el;
component.els = [el];
componentLookup[id] = component;
}
if (componentDef.___willRerenderInBrowser) {
if (componentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER) {
component.___rerender(true);
return;
}
@ -106,7 +117,7 @@ function initComponent(componentDef, doc) {
var eventType = domEventArgs[0];
var targetMethodName = domEventArgs[1];
var eventEl = getElementById(doc, domEventArgs[2]);
var eventEl = component.___keyedElements[domEventArgs[2]];
var extraArgs = domEventArgs[3];
addDOMEventListeners(component, eventEl, eventType, targetMethodName, extraArgs, eventListenerHandles);
@ -139,13 +150,8 @@ function initClientRendered(componentDefs, doc) {
eventDelegation.___init(doc);
doc = doc || defaultDocument;
for (var i=0,len=componentDefs.length; i<len; i++) {
for (var i=componentDefs.length-1; i>=0; i--) {
var componentDef = componentDefs[i];
if (componentDef.___children) {
initClientRendered(componentDef.___children, doc);
}
initComponent(
componentDef,
doc);
@ -172,9 +178,12 @@ function initServerRendered(renderedComponents, doc) {
return;
}
doc = doc || defaultDocument;
// Ensure that event handlers to handle delegating events are
// always attached before initializing any components
eventDelegation.___init(doc || defaultDocument);
eventDelegation.___init(doc);
renderedComponents = warp10Finalize(renderedComponents);
@ -188,6 +197,65 @@ function initServerRendered(renderedComponents, doc) {
componentDefs.forEach(function(componentDef) {
componentDef = ComponentDef.___deserialize(componentDef, typesArray, serverRenderedGlobals, registry);
var componentId = componentDef.id;
var component = componentDef.___component;
var startNode;
var endNode;
var flags = componentDef.___flags;
if ((flags & 6) === 6) {
startNode = document.head;
endNode = document.body;
} else if (flags & FLAG_HAS_BODY_EL) {
startNode = endNode = document.body;
} else if (flags & FLAG_HAS_HEAD_EL) {
startNode = endNode = document.head;
} else {
var startNodeComment = serverComponentStartNodes[componentId];
if (!startNodeComment) {
indexServerComponentBoundaries(doc);
startNodeComment = serverComponentStartNodes[componentId];
}
var endNodeComment = serverComponentEndNodes[componentId];
startNode = startNodeComment.nextSibling;
if (startNode === endNodeComment) {
// Component has no output nodes so just mount to the start comment node
// and we will remove the end comment node
startNode = endNode = startNodeComment;
} else {
startNodeComment.parentNode.removeChild(startNodeComment);
if (startNode.parentNode === document) {
endNode = startNode = document.documentElement;
} else {
// Remove the start and end comment nodes and use the inner nodes
// as the boundary
endNode = endNodeComment.previousSibling;
}
}
if (endNodeComment) {
endNodeComment.parentNode.removeChild(endNodeComment);
}
}
component.___keyedElements = keyedElementsByComponentId[componentId] || {};
component.___startNode = startNode;
component.___endNode = endNode;
delete keyedElementsByComponentId[componentId];
// Mark the start node so that we know we need to skip past this
// node when matching up children
startNode.___startNode = true;
// Mark the end node so that when we attempt to find boundaries
// for nested UI components we don't accidentally go outside the boundary
// of the parent component
endNode.___endNode = true;
initComponent(componentDef, doc || defaultDocument);
});
}

View File

@ -23,11 +23,11 @@ exports.patchComponent = function(jQuery) {
var match = idRegExp.exec(arg);
//Reset the search to 0 so the next call to exec will start from the beginning for the new string
if (match != null) {
var componentElId = match[1];
var key = match[1];
if (match[2] == null) {
return jQuery(self.getEl(componentElId));
return jQuery(self.getEl(key));
} else {
return jQuery('#' + self.getElId(componentElId) + match[2]);
return jQuery(match[2].trim(), self.getEl(key));
}
} else {
var rootEl = self.getEl();

View File

@ -4,8 +4,9 @@ var componentLookup = componentsUtil.___componentLookup;
var registry = require('../registry');
var modernRenderer = require('../renderer');
var resolveComponentKey = modernRenderer.___resolveComponentKey;
var preserveComponentEls = modernRenderer.___preserveComponentEls;
var handleBeginAsync = modernRenderer.___handleBeginAsync;
var beginComponent = require('../beginComponent');
var endComponent = require('../endComponent');
var WIDGETS_BEGIN_ASYNC_ADDED_KEY = '$wa';
@ -13,6 +14,7 @@ function createRendererFunc(templateRenderFunc, componentProps) {
var typeName = componentProps.type;
var roots = componentProps.roots;
var assignedId = componentProps.id;
var isSplit = componentProps.split === true;
return function renderer(input, out, renderingLogic) {
var outGlobal = out.global;
@ -44,39 +46,46 @@ function createRendererFunc(templateRenderFunc, componentProps) {
var globalComponentsContext = componentsContext.___globalContext;
var component = globalComponentsContext.___rerenderComponent;
var fakeComponent;
var isRerender = component !== undefined;
var id = assignedId;
var isExisting;
var customEvents;
var scope;
var parentComponentDef;
if (component) {
id = component.id;
isExisting = true;
globalComponentsContext.___rerenderComponent = null;
} else {
parentComponentDef = componentsContext.___componentDef;
var componentArgs = out.___componentArgs;
if (componentArgs) {
scope = parentComponentDef.id;
out.___componentArgs = null;
scope = componentArgs[0];
if (scope) {
scope = scope.id;
var key;
if (typeof componentArgs === 'string') {
key = componentArgs;
} else {
key = componentArgs[0];
customEvents = componentArgs[1];
}
var ref = componentArgs[1];
if (ref != null) {
ref = ref.toString();
if (key != null) {
key = key.toString();
}
id = id || resolveComponentKey(globalComponentsContext, ref, scope);
customEvents = componentArgs[2];
id = id || resolveComponentKey(globalComponentsContext, key, parentComponentDef);
} else if (parentComponentDef) {
id = parentComponentDef.___nextComponentId();
} else {
id = globalComponentsContext.___nextComponentId();
}
}
id = id || componentsContext.___nextComponentId();
if (registry.___isServer && typeName) {
component = { id:id, typeName:typeName };
} else {
@ -101,6 +110,10 @@ function createRendererFunc(templateRenderFunc, componentProps) {
}
}
if (component) {
component.___updateQueued = true;
}
if (input) {
if (getWidgetConfig) {
// If getWidgetConfig() was implemented then use that to
@ -151,14 +164,23 @@ function createRendererFunc(templateRenderFunc, componentProps) {
component.___setCustomEvents(customEvents, scope);
}
preserveComponentEls(component, out, globalComponentsContext);
// We put a placeholder element in the output stream to ensure that the existing
// DOM node is matched up correctly when using morphdom. We flag the VElement
// node to track that it is a preserve marker
out.___preserveComponent(component);
globalComponentsContext.___renderedComponentsById[id] = true;
component.___reset(); // The component is no longer dirty so reset internal flags
return;
}
}
var isFakeComponent = false;
if (!component) {
fakeComponent = {
id: id
isFakeComponent = true;
component = {
id: id,
___keyedElements: {}
};
} else {
componentState = component.___rawState || componentState;
@ -168,17 +190,22 @@ function createRendererFunc(templateRenderFunc, componentProps) {
getTemplateData(componentState, input, out) :
componentState || input || {};
var componentDef = componentsContext.___beginComponent(component || fakeComponent);
var componentDef = beginComponent(componentsContext, component, isSplit, parentComponentDef);
// This is a hack, but we have to swap out the component instance stored with this node
var vComponentNode = out.___parent;
componentDef.___roots = roots;
componentDef.___component = fakeComponent ? null : component;
componentDef.___component = isFakeComponent ? null : component;
componentDef.___isExisting = isExisting;
componentDef.b = componentBody;
componentDef.c = function(widgetConfig) {
component.$c = widgetConfig;
};
componentDef.t = function(typeName) {
if (typeName) {
this.___component = component = registry.___createComponent(typeName, fakeComponent.id);
vComponentNode.___component = this.___component = component = registry.___createComponent(typeName, component.id);
}
};
@ -188,7 +215,7 @@ function createRendererFunc(templateRenderFunc, componentProps) {
// Render the template associated with the component using the final template
// data that we constructed
templateRenderFunc(templateInput, out, componentDef, componentDef);
templateRenderFunc(templateInput, out, componentDef, componentDef, component);
if (customEvents && componentDef.___component) {
if (registry.___isServer) {
@ -199,7 +226,8 @@ function createRendererFunc(templateRenderFunc, componentProps) {
}
}
componentDef.___end();
endComponent(out, componentDef);
componentsContext.___componentDef = parentComponentDef;
};
}

View File

@ -1,6 +1,7 @@
{
"browser": {
"./beginComponent.js": "./beginComponent-browser.js",
"./endComponent.js": "./endComponent-browser.js",
"./helpers.js": "./helpers-browser.js",
"./index.js": "./index-browser.js",
"./init-components.js": "./init-components-browser.js",

View File

@ -4,50 +4,26 @@ var emitLifecycleEvent = componentsUtil.___emitLifecycleEvent;
var ComponentsContext = require('./ComponentsContext');
var getComponentsContext = ComponentsContext.___getComponentsContext;
var repeatedRegExp = /\[\]$/;
var registry = require('./registry');
var copyProps = require('raptor-util/copyProps');
var isServer = componentsUtil.___isServer === true;
var beginComponent = require('./beginComponent');
var endComponent = require('./endComponent');
var COMPONENT_BEGIN_ASYNC_ADDED_KEY = '$wa';
function resolveComponentKey(globalComponentsContext, key, scope) {
if (key[0] == '#') {
function resolveComponentKey(globalComponentsContext, key, parentComponentDef) {
if (key[0] === '#') {
return key.substring(1);
} else {
var resolvedId;
if (repeatedRegExp.test(key)) {
resolvedId = globalComponentsContext.___nextRepeatedId(scope, key);
} else {
resolvedId = scope + '-' + key;
}
return resolvedId;
return parentComponentDef.id + '-' + parentComponentDef.___nextKey(key);
}
}
function preserveComponentEls(existingComponent, out, globalComponentsContext) {
var rootEls = existingComponent.___getRootEls({});
for (var elId in rootEls) {
var el = rootEls[elId];
// We put a placeholder element in the output stream to ensure that the existing
// DOM node is matched up correctly when using morphdom.
out.element(el.tagName, { id: elId });
globalComponentsContext.___preserveDOMNode(elId); // Mark the element as being preserved (for morphdom)
}
existingComponent.___reset(); // The component is no longer dirty so reset internal flags
return true;
}
function handleBeginAsync(event) {
var parentOut = event.parentOut;
var asyncOut = event.out;
var componentsContext = parentOut.data.___components;
var componentsContext = parentOut.___components;
if (componentsContext !== undefined) {
// All of the components in this async block should be
@ -58,8 +34,7 @@ function handleBeginAsync(event) {
// stack (to begin with). This will result in top-level components
// of the async block being added as children of the component in the
// parent block.
var nestedComponentsContext = componentsContext.___createNestedComponentsContext(asyncOut);
asyncOut.data.___components = nestedComponentsContext;
asyncOut.___components = new ComponentsContext(asyncOut, componentsContext);
}
// Carry along the component arguments
asyncOut.___componentArgs = parentOut.___componentArgs;
@ -69,8 +44,6 @@ function createRendererFunc(templateRenderFunc, componentProps, renderingLogic)
renderingLogic = renderingLogic || {};
var onInput = renderingLogic.onInput;
var typeName = componentProps.type;
var roots = componentProps.roots;
var assignedId = componentProps.id;
var isSplit = componentProps.split === true;
var shouldApplySplitMixins = isSplit;
@ -89,38 +62,46 @@ function createRendererFunc(templateRenderFunc, componentProps, renderingLogic)
var component = globalComponentsContext.___rerenderComponent;
var isRerender = component !== undefined;
var id = assignedId;
var id;
var isExisting;
var customEvents;
var scope;
var parentComponentDef;
if (component) {
id = component.id;
isExisting = true;
globalComponentsContext.___rerenderComponent = null;
} else {
parentComponentDef = componentsContext.___componentDef;
var componentArgs = out.___componentArgs;
if (componentArgs) {
// console.log('componentArgs:', componentArgs);
scope = parentComponentDef.id;
out.___componentArgs = null;
scope = componentArgs[0];
var key;
if (scope) {
scope = scope.id;
if (typeof componentArgs === 'string') {
key = componentArgs;
} else {
key = componentArgs[0];
customEvents = componentArgs[1];
}
var key = componentArgs[1];
if (key != null) {
key = key.toString();
id = resolveComponentKey(globalComponentsContext, key, parentComponentDef);
} else {
id = parentComponentDef.___nextComponentId();
}
id = id || resolveComponentKey(globalComponentsContext, key, scope);
customEvents = componentArgs[2];
} else if (parentComponentDef) {
id = parentComponentDef.___nextComponentId();
} else {
id = globalComponentsContext.___nextComponentId();
}
}
id = id || componentsContext.___nextComponentId();
if (isServer) {
component = registry.___createComponent(
renderingLogic,
@ -134,12 +115,10 @@ function createRendererFunc(templateRenderFunc, componentProps, renderingLogic)
component.___updatedInput = undefined; // We don't want ___updatedInput to be serialized to the browser
} else {
if (!component) {
if (isRerender) {
// Look in in the DOM to see if a component with the same ID and type already exists.
component = componentLookup[id];
if (component && component.___type !== typeName) {
component = undefined;
}
if (isRerender && (component = componentLookup[id]) && component.___type !== typeName) {
// Destroy the existing component since
component.destroy();
component = undefined;
}
if (component) {
@ -178,7 +157,12 @@ function createRendererFunc(templateRenderFunc, componentProps, renderingLogic)
if (isExisting === true) {
if (component.___isDirty === false || component.shouldUpdate(input, component.___state) === false) {
preserveComponentEls(component, out, globalComponentsContext);
// We put a placeholder element in the output stream to ensure that the existing
// DOM node is matched up correctly when using morphdom. We flag the VElement
// node to track that it is a preserve marker
out.___preserveComponent(component);
globalComponentsContext.___renderedComponentsById[id] = true;
component.___reset(); // The component is no longer dirty so reset internal flags
return;
}
}
@ -189,15 +173,17 @@ function createRendererFunc(templateRenderFunc, componentProps, renderingLogic)
emitLifecycleEvent(component, 'render', out);
}
var componentDef = componentsContext.___beginComponent(component, isSplit);
componentDef.___roots = roots;
var componentDef =
beginComponent(componentsContext, component, isSplit, parentComponentDef);
componentDef.___isExisting = isExisting;
// Render the template associated with the component using the final template
// data that we constructed
templateRenderFunc(input, out, componentDef, component, component.___rawState);
componentDef.___end();
endComponent(out, componentDef);
componentsContext.___componentDef = parentComponentDef;
};
}
@ -205,5 +191,4 @@ module.exports = createRendererFunc;
// exports used by the legacy renderer
createRendererFunc.___resolveComponentKey = resolveComponentKey;
createRendererFunc.___preserveComponentEls = preserveComponentEls;
createRendererFunc.___handleBeginAsync = handleBeginAsync;

View File

@ -2,23 +2,15 @@
class ComponentArgs {
constructor() {
this.id = null;
this.key = null;
this.customEvents = null;
this.empty = true;
}
setId(id) {
this.empty = false;
this.id = id;
}
getId() {
return this.id;
setKey(key) {
this.key = key;
}
addCustomEvent(eventType, targetMethod, extraArgs) {
this.empty = false;
if (!this.customEvents) {
this.customEvents = [];
}
@ -27,7 +19,7 @@ class ComponentArgs {
}
compile(transformHelper) {
if (this.empty) {
if (!this.key && !this.customEvents) {
return;
}
@ -35,29 +27,14 @@ class ComponentArgs {
var builder = transformHelper.builder;
var id = this.id;
var customEvents = this.customEvents;
var args;
// Make sure the nested component has access to the ID of the containing
// component if it is needed
var shouldProvideScope = id || customEvents;
var args = [];
if (shouldProvideScope) {
args.push(builder.identifier('__component'));
if (this.key && this.customEvents) {
args = builder.literal([ this.key, builder.literal(this.customEvents) ]);
} else if (this.customEvents) {
args = builder.literal([ builder.literalNull(), builder.literal(this.customEvents) ]);
} else {
args.push(builder.literalNull());
}
if (id != null) {
args.push(id);
} else {
args.push(builder.literalNull());
}
if (customEvents) {
args.push(builder.literal(customEvents));
args = this.key;
}
if (el.type === 'CustomTag') {
@ -66,7 +43,7 @@ class ComponentArgs {
el.generateRenderTagCode = function(codegen, tagVar, tagArgs) {
tagArgs = [tagVar].concat(tagArgs);
tagArgs.push(builder.literal(args));
tagArgs.push(args);
return codegen.builder.functionCall(
renderComponentHelper,
@ -75,7 +52,7 @@ class ComponentArgs {
} else {
el.onBeforeGenerateCode((event) => {
let funcTarget = builder.memberExpression(builder.identifierOut(), builder.identifier('c'));
let funcArgs = [builder.literal(args)];
let funcArgs = [args];
event.insertCode(builder.functionCall(funcTarget, funcArgs));
});

View File

@ -12,7 +12,11 @@ module.exports = function assignComponentId(isRepeated) {
var context = this.context;
var builder = this.builder;
let componentRef;
if (el.noOutput || (el.tagDef && el.tagDef.noOutput)) {
return;
}
let assignedKey;
var nestedIdExpression;
var idExpression;
@ -27,6 +31,9 @@ module.exports = function assignComponentId(isRepeated) {
this.getMarkoComponentsRequirePath('marko/components/taglib/helpers/getCurrentComponent'));
context.addVar('__component', builder.functionCall(getCurrentComponentVar, [builder.identifierOut()]));
context.addVar('component', builder.memberExpression(
builder.identifier('__component'),
builder.identifier('___component')));
}
}
@ -42,72 +49,80 @@ module.exports = function assignComponentId(isRepeated) {
// 3) The HTML does not have an "id" or "ref" attribute. We must add
// an "id" attribute with a unique ID.
var isCustomTag = el.type !== 'HtmlElement';
var isHtmlElement = el.type === 'HtmlElement';
var isCustomTag = el.type === 'CustomTag';
if (el.hasAttribute('key')) {
componentRef = el.getAttributeValue('key');
el.removeAttribute('key');
} else if (el.hasAttribute('ref')) {
context.deprecate('The "ref" attribute is deprecated. Please use "key" instead.');
componentRef = el.getAttributeValue('ref');
el.removeAttribute('ref');
// LEGACY -- Remove in Marko 5.0
if (!isCustomTag && el.tagName === 'invoke') {
isCustomTag = true;
}
if (!isCustomTag && !isHtmlElement) {
return;
}
if (el.hasAttribute('w-id')) {
context.deprecate('The "w-id" attribute is deprecated. Please use "key" instead.');
if (componentRef) {
this.addError('The "w-id" attribute cannot be used in conjuction with the "ref" or "key" attributes.');
if (el.hasAttribute('key')) {
this.addError('The "w-id" attribute cannot be used in conjunction with the "key" attributes.');
return;
}
componentRef = el.getAttributeValue('w-id');
if (el.hasAttribute('ref')) {
this.addError('The "w-id" attribute cannot be used in conjunction with the "ref" attributes.');
return;
}
assignedKey = el.getAttributeValue('w-id');
el.removeAttribute('w-id');
} else if (el.hasAttribute('key')) {
assignedKey = el.getAttributeValue('key');
el.removeAttribute('key');
} else if (el.hasAttribute('ref')) {
context.deprecate('The "ref" attribute is deprecated. Please use "key" instead.');
assignedKey = el.getAttributeValue('ref');
el.removeAttribute('ref');
}
if (componentRef) {
idExpression = this.buildComponentElIdFunctionCall(componentRef);
nestedIdExpression = componentRef;
if (assignedKey) {
nestedIdExpression = assignedKey;
if (isCustomTag) {
idExpression = this.buildComponentElIdFunctionCall(assignedKey);
// The element is a custom tag
this.getComponentArgs().setId(nestedIdExpression);
this.getComponentArgs().setKey(nestedIdExpression);
} else {
if (el.hasAttribute('id')) {
this.addError('The "ref", "key", and "w-id" attributes cannot be used in conjuction with the "id" attribute.');
return;
idExpression = assignedKey;
if (context.data.hasLegacyForKey && el.data.userAssignedKey !== false) {
el.setAttributeValue('id', this.buildComponentElIdFunctionCall(assignedKey));
}
el.setAttributeValue('id', idExpression);
}
} else if (el.hasAttribute('id')) {
idExpression = el.getAttributeValue('id');
if (el.isFlagSet('hasComponentBind')) {
// We have to attach a listener to the root element of the component
// We will use an empty string as an indicator that it is the root component
// element.
nestedIdExpression = builder.literal('');
} else {
// Convert the raw String to a JavaScript expression. we need to prefix
// with '#' to make it clear this is a fully resolved element ID
nestedIdExpression = builder.concat(
builder.literal('#'),
idExpression);
if (context.isServerTarget()) {
var markoKeyAttrVar = context.importModule('marko_keyAttr',
this.getMarkoComponentsRequirePath('marko/components/taglib/helpers/markoKeyAttr'));
el.setAttributeValue('data-marko-key', builder.functionCall(markoKeyAttrVar, [
idExpression,
builder.identifier('__component')
]));
}
el.setKey(assignedKey);
}
} else {
// Case 3 - We need to add a unique "id" attribute
let uniqueElId = this.nextUniqueId();
nestedIdExpression = isRepeated ? builder.literal(uniqueElId + '[]') : builder.literal(uniqueElId);
nestedIdExpression = isRepeated ? builder.literal(uniqueElId + '[]') : builder.literal(uniqueElId.toString());
idExpression = this.buildComponentElIdFunctionCall(nestedIdExpression);
idExpression = builder.literal(uniqueElId.toString());
if (isCustomTag) {
this.getComponentArgs().setId(nestedIdExpression);
this.getComponentArgs().setKey(nestedIdExpression);
} else {
el.setAttributeValue('id', idExpression);
el.setKey(idExpression);
}
}
@ -123,13 +138,17 @@ module.exports = function assignComponentId(isRepeated) {
}
let uniqueElId = transformHelper.nextUniqueId();
let idVarName = '__componentId' + uniqueElId;
let idVarName = '__key' + uniqueElId;
let idVar = builder.identifier(idVarName);
this.idVarNode = builder.vars([
{
id: idVarName,
init: idExpression
init: builder.functionCall(
builder.memberExpression(
builder.identifier('__component'),
builder.identifier('___nextKey')),
[ idExpression ])
}
]);
@ -140,9 +159,9 @@ module.exports = function assignComponentId(isRepeated) {
idVar);
if (isCustomTag) {
transformHelper.getComponentArgs().setId(nestedIdExpression);
transformHelper.getComponentArgs().setKey(nestedIdExpression);
} else {
el.setAttributeValue('id', idExpression);
el.setKey(idExpression);
}
return this.idVarNode;

View File

@ -0,0 +1,158 @@
'use strict';
const generateRegisterComponentCode = require('../util/generateRegisterComponentCode');
// var FLAG_WILL_RERENDER_IN_BROWSER = 1;
var FLAG_HAS_BODY_EL = 2;
var FLAG_HAS_HEAD_EL = 4;
module.exports = function handleComponentBind(options) {
if (this.firstBind) {
return;
}
this.firstBind = true;
let context = this.context;
let builder = this.builder;
let isLegacyComponent = this.isLegacyComponent = options.isLegacyComponent === true;
let componentModule = options.componentModule;
let rendererModule = options.rendererModule;
let componentProps = options.componentProps || {};
let rootNodes = options.rootNodes;
let isLegacyInnerBind = options.isLegacyInnerBind;
var isSplit = false;
if ((rendererModule && rendererModule !== componentModule) ||
(!rendererModule && componentModule)) {
componentProps.split = isSplit = true;
}
if (componentModule) {
let componentTypeNode;
let dependencyModule = isLegacyComponent || isSplit ? componentModule : this.getTemplateModule();
if (dependencyModule.requirePath) {
context.addDependency({ type:'require', path: dependencyModule.requirePath });
}
if (isSplit) {
context.addDependency({ type:'require', path: context.markoModulePrefix + 'components' });
}
componentTypeNode = context.addStaticVar(
'marko_componentType',
generateRegisterComponentCode(componentModule, this, isSplit));
componentProps.type = componentTypeNode;
}
if (isLegacyInnerBind) {
let el = rootNodes[0];
el.setAttributeValue('id',
builder.memberExpression(
builder.identifier('__component'),
builder.identifier('id')));
// TODO Deprecation warning for inner binds
let componentNode = context.createNodeForEl('_component', {
props: builder.literal(componentProps)
});
el.wrapWith(componentNode);
return;
}
var flags = 0;
rootNodes.forEach((rootNode) => {
if (rootNode.type === 'HtmlElement') {
if (rootNode.tagName === 'body') {
flags |= FLAG_HAS_BODY_EL;
} else if (rootNode.tagName === 'head') {
flags |= FLAG_HAS_HEAD_EL;
}
}
});
if (flags) {
context.root.appendChild(
builder.assignment(
builder.memberExpression(
builder.identifier('__component'),
builder.identifier('___flags')),
builder.literal(flags),
'|='));
}
let markoComponentVar;
if (rendererModule) {
if (rendererModule.inlineId) {
markoComponentVar = rendererModule.inlineId;
} else {
markoComponentVar = context.addStaticVar('marko_component', builder.require(builder.literal(rendererModule.requirePath)));
}
}
this.setHasBoundComponentForTemplate();
var rendererHelper = isLegacyComponent ?
this.context.helper('rendererLegacy') :
this.context.helper('renderer');
var defineComponentHelper;
if (!isSplit && !isLegacyComponent) {
defineComponentHelper = this.context.helper('defineComponent');
}
this.context.on('beforeGenerateCode:TemplateRoot', function(eventArgs) {
eventArgs.node.addRenderFunctionParam(builder.identifier('__component'));
if (isLegacyComponent) {
eventArgs.node.addRenderFunctionParam(builder.identifier('widget'));
eventArgs.node.addRenderFunctionParam(builder.identifier('component'));
} else {
eventArgs.node.addRenderFunctionParam(builder.identifier('component'));
eventArgs.node.addRenderFunctionParam(builder.identifier('state'));
}
eventArgs.node.generateAssignRenderCode = (eventArgs) => {
const nodes = [];
const templateVar = eventArgs.templateVar;
const templateRendererMember = eventArgs.templateRendererMember;
const renderFunctionVar = eventArgs.renderFunctionVar;
const createRendererArgs = [
renderFunctionVar,
builder.literal(componentProps)
];
if (markoComponentVar) {
createRendererArgs.push(markoComponentVar);
}
nodes.push(builder.assignment(
templateRendererMember,
builder.functionCall(
rendererHelper,
createRendererArgs)));
if (!isSplit && !isLegacyComponent) {
nodes.push(builder.assignment(
builder.memberExpression(templateVar, builder.identifier('Component')),
builder.functionCall(
defineComponentHelper,
[
markoComponentVar,
templateRendererMember
])));
}
return nodes;
};
});
};

View File

@ -1,287 +0,0 @@
'use strict';
const resolveFrom = require('resolve-from');
const generateRegisterComponentCode = require('../util/generateRegisterComponentCode');
function legacyGetDefaultComponentModule(dirname) {
var filename;
var legacy = true;
if ((filename = resolveFrom(dirname, './widget'))) {
return {
filename,
requirePath: './widget',
legacy
};
} else if ((filename = resolveFrom(dirname, './component'))) {
return {
filename,
requirePath: './component',
legacy
};
} else if ((filename = resolveFrom(dirname, './'))) {
return {
filename,
requirePath: './',
legacy
};
} else {
return null;
}
}
function checkIsInnerBind(el) {
var curNode = el;
while (true) {
if (curNode.data.hasBoundComponent) {
return true;
}
curNode = curNode.parentNode;
if (!curNode) {
break;
}
}
return false;
}
module.exports = function handleComponentBind() {
let el = this.el;
let context = this.context;
let builder = this.builder;
let componentModule;
let rendererModulePath;
let rendererModule = this.getRendererModule();
let isLegacyComponent = false;
if (el.hasAttribute('w-bind')) {
let bindAttr = el.getAttribute('w-bind');
context.deprecate('Legacy components using w-bind and defineRenderer/defineComponent or defineComponent are deprecated. See: https://github.com/marko-js/marko/issues/421');
this.isLegacyComponent = isLegacyComponent = true;
// Remove the w-bind attribute since we don't want it showing up in the output DOM
el.removeAttribute('w-bind');
// Read the value for the w-bind attribute. This will be an AST node for the parsed JavaScript
let bindAttrValue = bindAttr.value;
const hasWidgetTypes = context.isFlagSet('hasWidgetTypes');
if (hasWidgetTypes) {
context.deprecate('The <widget-types> tag is deprecated. Please remove it. See: https://github.com/marko-js/marko/issues/514');
}
if (bindAttrValue == null) {
componentModule = legacyGetDefaultComponentModule(this.dirname);
if (!componentModule) {
this.addError('No corresponding JavaScript module found in the same directory (either "component.js" or "index.js").');
return;
}
} else if (bindAttr.isLiteralValue()) {
if (typeof bindAttr.literalValue !== 'string') {
this.addError('The value for the "w-bind" attribute should be a string. Actual: ' + componentModule);
return;
}
let requirePath = bindAttr.literalValue;
let filename = resolveFrom(this.dirname, requirePath);
if (!filename) {
this.addError('Target file not found: ' + requirePath + ' (from: ' + this.dirname + ')');
return;
}
componentModule = {
legacy: true,
filename,
requirePath
};
} else {
// This is a dynamic expression. The <widget-types> should have been found.
if (!hasWidgetTypes) {
this.addError('The <widget-types> tag must be used to declare components when the value of the "w-bind" attribute is a dynamic expression.');
return;
}
el.insertSiblingBefore(
builder.functionCall(
builder.memberExpression(builder.identifier('__component'), builder.identifier('t')),
[
builder.memberExpression(
builder.identifier('marko_componentTypes'),
bindAttrValue,
true /* computed */)
]));
}
} else if (el.isFlagSet('hasComponentBind')) {
componentModule = this.getComponentModule();
rendererModulePath = this.getRendererModule();
if (context.isFlagSet('hasWidgetTypes')) {
context.addError('The <widget-types> tag is no longer supported. See: https://github.com/marko-js/marko/issues/514');
}
} else {
return;
}
this.setHasBoundComponentForTemplate();
let isInnerBind = checkIsInnerBind(el.parentNode);
el.data.hasBoundComponent = true;
// A component is bound to the el...
var componentProps = isInnerBind ? {} : this.getComponentProps();
let transformHelper = this;
var isSplit = false;
if ((rendererModule && rendererModule !== componentModule) ||
(!rendererModule && componentModule)) {
componentProps.split = isSplit = true;
}
if (componentModule) {
let componentTypeNode;
let dependencyModule = isLegacyComponent || isSplit ? componentModule : this.getTemplateModule();
if (dependencyModule.requirePath) {
context.addDependency({ type:'require', path: dependencyModule.requirePath });
}
if (isSplit) {
context.addDependency({ type:'require', path: context.markoModulePrefix + 'components' });
}
componentTypeNode = context.addStaticVar(
'marko_componentType',
generateRegisterComponentCode(componentModule, this, isSplit));
componentProps.type = componentTypeNode;
}
if (el.hasAttribute('w-config')) {
el.insertSiblingBefore(
builder.functionCall(
builder.memberExpression(builder.identifier('__component'), builder.identifier('c')),
[
el.getAttributeValue('w-config')
]));
el.removeAttribute('w-config');
}
let id = el.getAttributeValue('id');
if (id) {
componentProps.id = id;
}
let markoComponentVar;
if (rendererModule) {
if (rendererModule.inlineId) {
markoComponentVar = rendererModule.inlineId;
} else {
markoComponentVar = context.addStaticVar('marko_component', builder.require(builder.literal(rendererModule.requirePath)));
}
}
if (isInnerBind) {
el.setAttributeValue('id',
builder.memberExpression(
builder.identifier('__component'),
builder.identifier('id')));
// TODO Deprecation warning for inner binds
let componentNode = context.createNodeForEl('_component', {
props: builder.literal(componentProps)
});
el.wrapWith(componentNode);
return;
}
if (this.firstBind) {
var rendererHelper = isLegacyComponent ?
transformHelper.context.helper('rendererLegacy') :
transformHelper.context.helper('renderer');
var defineComponentHelper;
if (!isSplit && !isLegacyComponent) {
defineComponentHelper = transformHelper.context.helper('defineComponent');
}
this.context.on('beforeGenerateCode:TemplateRoot', function(eventArgs) {
eventArgs.node.addRenderFunctionParam(builder.identifier('__component'));
if (isLegacyComponent) {
eventArgs.node.addRenderFunctionParam(builder.identifier('widget'));
} else {
eventArgs.node.addRenderFunctionParam(builder.identifier('component'));
eventArgs.node.addRenderFunctionParam(builder.identifier('state'));
}
eventArgs.node.generateAssignRenderCode = function(eventArgs) {
let nodes = [];
let templateVar = eventArgs.templateVar;
let templateRendererMember = eventArgs.templateRendererMember;
let renderFunctionVar = eventArgs.renderFunctionVar;
let createRendererArgs = [
renderFunctionVar,
builder.literal(componentProps)
];
if (markoComponentVar) {
createRendererArgs.push(markoComponentVar);
}
nodes.push(builder.assignment(
templateRendererMember,
builder.functionCall(
rendererHelper,
createRendererArgs)));
if (!isSplit && !isLegacyComponent) {
nodes.push(builder.assignment(
builder.memberExpression(templateVar, builder.identifier('Component')),
builder.functionCall(
defineComponentHelper,
[
markoComponentVar,
templateRendererMember
])));
}
return nodes;
};
});
}
if (el.hasAttribute('key')) {
if (!componentProps.roots) {
componentProps.roots = [];
}
var key = el.getAttributeValue('key');
componentProps.roots.push(key);
} else if (el.hasAttribute('ref')) {
if (!componentProps.roots) {
componentProps.roots = [];
}
var ref = el.getAttributeValue('ref');
componentProps.roots.push(ref);
} else {
el.setAttributeValue('id',
builder.memberExpression(
builder.identifier('__component'),
builder.identifier('id')));
}
};

View File

@ -1,51 +0,0 @@
'use strict';
const keySuffix = ':key';
module.exports = function handleComponentKeyAttrs() {
let el = this.el;
let context = this.context;
const filePosition = el.pos ? context.getPosInfo(el.pos) : context.filename;
// BEGIN support for deprecated for attributes
let deprecatedForAttributes = ['for-ref', 'for-key', 'w-for'];
deprecatedForAttributes.forEach(attributeName => {
if (el.hasAttribute(attributeName)) {
context.deprecate(`The "${attributeName}" tag is deprecated. Please use "for:key" instead.`);
let incompatibleAttributes = ['for', 'for:key']
.concat(deprecatedForAttributes.filter(a => a != attributeName))
.filter(a => el.hasAttribute(a));
if (incompatibleAttributes.length) {
this.addError(`The "${attributeName}" tag cannot be used with "${incompatibleAttributes.join('" or "')}". (${filePosition})`);
} else {
el.setAttributeValue('for:key', el.getAttributeValue(attributeName));
}
el.removeAttribute(attributeName);
}
});
// END support for deprecated for attributes
el.attributes.forEach(attribute => {
const attributeName = attribute.name;
if (attributeName && attributeName !== keySuffix && attributeName.endsWith(keySuffix)) {
const unfixedName = attributeName.replace(keySuffix, '');
el.removeAttribute(attributeName);
if (el.hasAttribute(unfixedName)) {
this.addError(`The "${attributeName}" attribute cannot be used in conjuction with the "${unfixedName}" attribute. (${filePosition})`);
} else {
el.setAttributeValue(
unfixedName,
this.buildComponentElIdFunctionCall(attribute.value));
}
}
});
};

View File

@ -15,12 +15,17 @@ function addPreserve(transformHelper, bodyOnly, condition) {
preserveAttrs['if'] = condition;
}
let componentIdInfo = transformHelper.assignComponentId(true /* repeated */);
let componentIdInfo = transformHelper.assignComponentId();
let idVarNode = componentIdInfo.idVarNode ? null : componentIdInfo.createIdVarNode();
preserveAttrs.id = transformHelper.getIdExpression();
if (el.type === 'HtmlElement') {
preserveAttrs.key = transformHelper.getIdExpression();
} else {
preserveAttrs.cid = transformHelper.getIdExpression();
}
let preserveNode = context.createNodeForEl('w-preserve', preserveAttrs);
let preserveNode = context.createNodeForEl('_preserve', preserveAttrs);
let idVarNodeTarget;
if (bodyOnly) {

View File

@ -1,75 +0,0 @@
'use strict';
var includeTagForComponents = require.resolve('../include-tag');
module.exports = function(includeNode) {
var context = this.context;
if (!this.hasBoundComponentForTemplate()) {
return;
}
var parentNode = includeNode.parentNode;
if (!parentNode.hasAttribute) {
return;
}
parentNode._normalizeChildTextNodes(context, true /* force trim */);
if (parentNode.childCount === 1) {
if (includeNode.hasAttribute('key') || includeNode.hasAttribute('ref')) {
this.assignComponentId();
}
let parentTransformHelper = this.getTransformHelper(parentNode);
if (includeNode.data.bodySlot) {
parentTransformHelper.assignComponentId(false /* not repeated */);
var componentProps = this.getComponentProps();
componentProps.body = parentTransformHelper.getNestedIdExpression();
} else {
let componentIdInfo = parentTransformHelper.assignComponentId(true /* repeated */);
if (!componentIdInfo.idVarNode) {
let idVarNode = componentIdInfo.createIdVarNode();
parentNode.onBeforeGenerateCode((event) => {
event.insertCode(idVarNode);
});
}
}
includeNode.setRendererPath(includeTagForComponents);
includeNode.onBeforeGenerateCode(function() {
includeNode.addProp('_elId', parentTransformHelper.getIdExpression());
});
}
// includeNode.generateCodeForDynamicInclude = (options, codegen) => {
// var target = options.target;
// var data = options.data;
//
// if (!data) {
// data = builder.literal(null);
// }
//
// let includeVar = context.importModule('marko_component_include', this.getMarkoComponentsRequirePath('marko/components/taglib/helpers/include'));
//
// let includeArgs = [
// target,
// builder.identifierOut(),
// data
// ];
//
// if (parentTransformHelper) {
// includeArgs = includeArgs.concat([
// parentTransformHelper.getIdExpression(),
//
// ]);
// }
//
// return builder.functionCall(includeVar, includeArgs);
// };
};

View File

@ -0,0 +1,155 @@
'use strict';
const resolveFrom = require('resolve-from');
function legacyGetDefaultComponentModule(dirname) {
var filename;
var legacy = true;
if ((filename = resolveFrom(dirname, './widget'))) {
return {
filename,
requirePath: './widget',
legacy
};
} else if ((filename = resolveFrom(dirname, './component'))) {
return {
filename,
requirePath: './component',
legacy
};
} else if ((filename = resolveFrom(dirname, './'))) {
return {
filename,
requirePath: './',
legacy
};
} else {
return null;
}
}
function checkIsInnerBind(el) {
var curNode = el;
while (true) {
if (curNode.data.hasBoundComponent) {
return true;
}
curNode = curNode.parentNode;
if (!curNode) {
break;
}
}
return false;
}
module.exports = function handleLegacyBind() {
let el = this.el;
let context = this.context;
let builder = this.builder;
let componentModule;
let rendererModule;
let isLegacyComponent = false;
if (el.hasAttribute('w-bind')) {
let bindAttr = el.getAttribute('w-bind');
context.deprecate('Legacy components using w-bind and defineRenderer/defineComponent or defineComponent are deprecated. See: https://github.com/marko-js/marko/issues/421');
this.isLegacyComponent = isLegacyComponent = true;
// Remove the w-bind attribute since we don't want it showing up in the output DOM
el.removeAttribute('w-bind');
// Read the value for the w-bind attribute. This will be an AST node for the parsed JavaScript
let bindAttrValue = bindAttr.value;
const hasWidgetTypes = context.isFlagSet('hasWidgetTypes');
if (hasWidgetTypes) {
context.deprecate('The <widget-types> tag is deprecated. Please remove it. See: https://github.com/marko-js/marko/issues/514');
}
if (bindAttrValue == null) {
componentModule = legacyGetDefaultComponentModule(this.dirname);
if (!componentModule) {
this.addError('No corresponding JavaScript module found in the same directory (either "component.js" or "index.js").');
return;
}
} else if (bindAttr.isLiteralValue()) {
if (typeof bindAttr.literalValue !== 'string') {
this.addError('The value for the "w-bind" attribute should be a string. Actual: ' + componentModule);
return;
}
let requirePath = bindAttr.literalValue;
let filename = resolveFrom(this.dirname, requirePath);
if (!filename) {
this.addError('Target file not found: ' + requirePath + ' (from: ' + this.dirname + ')');
return;
}
componentModule = {
legacy: true,
filename,
requirePath
};
} else {
// This is a dynamic expression. The <widget-types> should have been found.
if (!hasWidgetTypes) {
this.addError('The <widget-types> tag must be used to declare components when the value of the "w-bind" attribute is a dynamic expression.');
return;
}
el.insertSiblingBefore(
builder.functionCall(
builder.memberExpression(builder.identifier('__component'), builder.identifier('t')),
[
builder.memberExpression(
builder.identifier('marko_componentTypes'),
bindAttrValue,
true /* computed */)
]));
}
} else {
return;
}
let isLegacyInnerBind = checkIsInnerBind(el.parentNode);
el.data.hasBoundComponent = true;
// A component is bound to the el...
if (el.hasAttribute('w-config')) {
el.insertSiblingBefore(
builder.functionCall(
builder.memberExpression(builder.identifier('__component'), builder.identifier('c')),
[
el.getAttributeValue('w-config')
]));
el.removeAttribute('w-config');
}
let componentProps = {};
let id = el.getAttributeValue('id');
if (id) {
componentProps.id = id;
}
this.convertToComponent({
isLegacyInnerBind,
componentModule,
rendererModule,
isLegacyComponent: true,
rootNodes: [el]
});
};

View File

@ -1,7 +1,7 @@
'use strict';
var path = require('path');
var getComponentFiles = require('./getComponentFiles');
let path = require('path');
let getComponentFiles = require('./getComponentFiles');
const esprima = require('esprima');
const escodegen = require('escodegen');
@ -12,16 +12,16 @@ function handleStyleElement(styleEl, transformHelper) {
return;
}
var attrs = styleEl.attributes;
let attrs = styleEl.attributes;
var styleCode;
var lang = 'css';
let styleCode;
let lang = 'css';
var hasStyleBlock = false;
let hasStyleBlock = false;
for (var i=attrs.length-1; i>=0; i--) {
var attr = attrs[i];
var name = attr.name;
for (let i=attrs.length-1; i>=0; i--) {
let attr = attrs[i];
let name = attr.name;
if (name.startsWith('{')) {
hasStyleBlock = true;
@ -47,7 +47,7 @@ function handleStyleElement(styleEl, transformHelper) {
styleEl.setFlag(FLAG_COMPONENT_STYLE);
if (styleCode) {
var context = transformHelper.context;
let context = transformHelper.context;
context.addDependency({
type: lang,
code: styleCode,
@ -95,15 +95,15 @@ function classToObject(cls, el, transformHelper) {
function handleClassDeclaration(classEl, transformHelper) {
let tree;
var wrappedSrc = '('+classEl.tagString+'\n)';
let wrappedSrc = '('+classEl.tagString+'\n)';
try {
tree = esprima.parse(wrappedSrc);
} catch(err) {
var message = 'Unable to parse JavaScript for component class. ' + err;
let message = 'Unable to parse JavaScript for component class. ' + err;
if (err.index != null) {
var errorIndex = err.index;
let errorIndex = err.index;
// message += '\n' + err.description;
if (errorIndex != null && errorIndex >= 0) {
transformHelper.context.addError({
@ -127,36 +127,28 @@ function handleClassDeclaration(classEl, transformHelper) {
let object = classToObject(expression, classEl, transformHelper);
let componentVar = transformHelper.context.addStaticVar('marko_component', escodegen.generate(object));
if (transformHelper.getRendererModule() != null) {
transformHelper.context.addError(classEl, 'The component has both an inline component `class` and a separate `component.js`. This is not allowed. See: https://github.com/marko-js/marko/wiki/Error:-Component-inline-and-external');
return;
}
var moduleInfo = {
let moduleInfo = {
inlineId: componentVar,
filename: transformHelper.filename,
requirePath: './' + path.basename(transformHelper.filename)
};
if (transformHelper.getComponentModule() == null) {
transformHelper.setComponentModule(moduleInfo);
}
transformHelper.setRendererModule(moduleInfo);
classEl.detach();
return moduleInfo;
}
module.exports = function handleRootNodes() {
var context = this.context;
var builder = this.builder;
let context = this.context;
var componentFiles = getComponentFiles(context.filename);
let componentFiles = getComponentFiles(context.filename);
if (!componentFiles) {
return;
}
var dirname = context.dirname;
let componentModule;
let rendererModule;
let dirname = context.dirname;
if (componentFiles.package) {
context.addDependency('package: ./' + componentFiles.package);
@ -174,67 +166,78 @@ module.exports = function handleRootNodes() {
requirePath: './'+file.slice(0, file.lastIndexOf('.'))
};
this.setComponentModule(moduleInfo);
this.setRendererModule(moduleInfo);
componentModule = rendererModule = moduleInfo;
}
if (componentFiles.browserFile) {
let file = componentFiles.browserFile;
this.setComponentModule({
componentModule = {
filename: path.join(dirname, file),
requirePath: './' + file.slice(
0, file.lastIndexOf('.'))
});
};
}
var templateRoot = this.el;
let templateRoot = this.el;
var rootNodes = [];
var hasLegacyExplicitBind = false;
var hasIdCount = 0;
var nodeWithAssignedId;
var assignedId;
var transformHelper = this;
let rootNodes = [];
let hasIdCount = 0;
let nodeWithAssignedId;
let assignedId;
let transformHelper = this;
let walker = context.createWalker({
enter(node) {
let tagName = node.tagName && node.tagName.toLowerCase();
let tag = node.tagName && context.taglibLookup.getTag(node.tagName);
if (node.type === 'TemplateRoot' || !node.type) {
// Don't worry about the TemplateRoot or a Container node
// But continue into the node to look at its children for root elements
} else if (tag && tag.noOutput) {
walker.skip();
// Don't worry about the TemplateRoot or an Container node
} else if (node.type === 'HtmlElement') {
if (node.hasAttribute('w-bind')) {
transformHelper.setHasBoundComponentForTemplate();
hasLegacyExplicitBind = true;
} else {
if (node.hasAttribute('id')) {
hasIdCount++;
nodeWithAssignedId = node;
assignedId = node.getAttributeValue('id');
}
if (node.hasAttribute('id')) {
hasIdCount++;
nodeWithAssignedId = node;
assignedId = node.getAttributeValue('id');
}
if (tagName === 'style') {
handleStyleElement(node, transformHelper);
}
if (tagName === 'style') {
handleStyleElement(node, transformHelper);
}
if (!node.isFlagSet(FLAG_COMPONENT_STYLE)) {
rootNodes.push(node);
}
if (!node.isFlagSet(FLAG_COMPONENT_STYLE)) {
rootNodes.push(node);
}
walker.skip();
return;
} else if (node.type === 'CustomTag') {
let tag = context.taglibLookup.getTag(node.tagName);
if (!tag.noOutput) {
rootNodes.push(node);
}
walker.skip();
return;
} else if (node.type === 'CustomTag') {
rootNodes.push(node);
} else if (node.type === 'Scriptlet') {
walker.skip();
return;
} else {
if (tagName === 'class') {
handleClassDeclaration(node, transformHelper);
let classComponentModule = handleClassDeclaration(node, transformHelper);
if (rendererModule != null) {
transformHelper.context.addError(node, 'The component has both an inline component `class` and a separate `component.js`. This is not allowed. See: https://github.com/marko-js/marko/wiki/Error:-Component-inline-and-external');
return;
}
if (componentModule == null) {
componentModule = classComponentModule;
}
rendererModule = classComponentModule;
} else if (!node.noOutput) {
rootNodes.push(node);
}
walker.skip();
@ -245,43 +248,29 @@ module.exports = function handleRootNodes() {
walker.walk(templateRoot);
if (hasLegacyExplicitBind) {
//There is an explicit bind so nothing to do
if (!componentModule) {
return;
}
if (!this.hasBoundComponentForTemplate()) {
return;
if (context.isFlagSet('hasWidgetTypes')) {
context.addError('The <widget-types> tag is no longer supported. See: https://github.com/marko-js/marko/issues/514');
}
templateRoot._normalizeChildTextNodes(context, true);
// After normalizing the text nodes to remove whitespace we may have detached
// some of the root text nodes so remove those from our list
rootNodes = rootNodes.filter((rootNode) => {
return rootNode.isDetached() !== true;
});
if (rootNodes.length === 0) {
return;
}
if (rootNodes.length > 1 && hasIdCount > 0) {
// We can only bind a component to multiple top-level elements if we can assign
// all of the IDs
return;
}
transformHelper.setHasBoundComponentForTemplate();
var nextKey = 0;
rootNodes.forEach((curNode, i) => {
let id = curNode.getAttributeValue('id');
if (id && id.type !== 'Literal') {
context.addError('Root HTML element should not have a dynamic `id` attribute. See: https://github.com/marko-js/marko/wiki/Error:-Dynamic-root-HTML-element-id-attribute');
return;
}
curNode.setFlag('hasComponentBind');
if (!curNode.hasAttribute('key') && !curNode.hasAttribute('ref')) {
if (curNode.type === 'CustomTag' || rootNodes.length > 1) {
curNode.setAttributeValue('key', builder.literal('_r' + (nextKey++)));
}
}
this.convertToComponent({
rootNodes,
componentModule,
rendererModule
});
};

View File

@ -0,0 +1,75 @@
'use strict';
const deprecatedKeySuffix = ':key';
const scopedSuffix = ':scoped';
const deprecatedAttrs = {
'for-ref': true,
'for-key': true,
'w-for': true
};
module.exports = function handleComponentKeyAttrs() {
let el = this.el;
let context = this.context;
let builder = this.builder;
const filePosition = el.pos ? context.getPosInfo(el.pos) : context.filename;
var attrs = el.attributes.concat([]);
attrs.forEach(attribute => {
const attributeName = attribute.name;
if (!attributeName) {
return;
}
let fixedAttributeName = attributeName;
// BEGIN support for deprecated for attributes
if (deprecatedAttrs[attributeName]) {
context.deprecate(`The "${attributeName}" attribute is deprecated. Please use "for:key" instead.`);
let incompatibleAttributes = ['for', 'for:key']
.concat(Object.keys(deprecatedAttrs).filter(a => a != attributeName))
.filter(a => el.hasAttribute(a));
if (incompatibleAttributes.length) {
this.addError(`The "${attributeName}" attribute cannot be used in conjunction with "${incompatibleAttributes.join('" or "')}". (${filePosition})`);
return;
} else {
fixedAttributeName = 'for:scoped';
}
} else if (attributeName !== deprecatedKeySuffix && attributeName.endsWith(deprecatedKeySuffix)) {
context.deprecate(`The "${attributeName}" attribute is deprecated. Please use "for:scoped" instead.`);
fixedAttributeName = attributeName.slice(0, 0-deprecatedKeySuffix.length) + ':scoped';
}
// END support for deprecated for attributes
if (fixedAttributeName !== scopedSuffix && fixedAttributeName.endsWith(scopedSuffix)) {
el.removeAttribute(attributeName);
let finalAttributeName = fixedAttributeName.slice(0, 0-scopedSuffix.length);
if (el.hasAttribute(finalAttributeName)) {
this.addError(`The "${attributeName}" attribute cannot be used in conjunction with the "${finalAttributeName}" attribute. (${filePosition})`);
return;
}
let uniqueElId = this.nextUniqueId();
let idVarName = 'marko_' + finalAttributeName + '_key' + uniqueElId;
let varNode = builder.var(idVarName, attribute.value);
el.insertSiblingBefore(varNode);
let varIdNode = builder.identifier(idVarName);
el.setAttributeValue(finalAttributeName, this.buildComponentElIdFunctionCall(varIdNode));
if (!el.hasAttribute('key') && !el.hasAttribute('w-id') && !el.hasAttribute('ref')) {
// The scoped attribute should be suitable for a key
el.setAttributeValue('key', varIdNode);
el.data.userAssignedKey = false;
}
}
});
};

View File

@ -24,25 +24,11 @@ class TransformHelper {
}
setHasBoundComponentForTemplate() {
this.context.isComponent = true;
this.context.data[HAS_COMPONENT_KEY] = true;
}
setComponentModule(value) {
this.context.data.componentModule = value;
}
getComponentModule() {
return this.context.data.componentModule;
}
setRendererModule(value) {
this.context.data.rendererModule = value;
}
getRendererModule() {
return this.context.data.rendererModule;
}
getTemplateModule() {
return {
requirePath:this.context.getRequirePath(this.filename)
@ -55,15 +41,6 @@ class TransformHelper {
this.context.data[WIDGET_PROPS_KEY] != null;
}
getComponentProps() {
var componentProps = this.context.data[WIDGET_PROPS_KEY];
if (!componentProps) {
this.firstBind = true;
componentProps = this.context.data[WIDGET_PROPS_KEY] = {};
}
return componentProps;
}
addError(message, code) {
this.context.addError(this.el, message, code);
}
@ -81,11 +58,6 @@ class TransformHelper {
return (this.context.data.componentNextElId++);
}
getNestedIdExpression() {
this.assignComponentId();
return this.getComponentIdInfo().nestedIdExpression;
}
getIdExpression() {
this.assignComponentId();
return this.getComponentIdInfo().idExpression;
@ -129,11 +101,19 @@ class TransformHelper {
buildComponentElIdFunctionCall(id) {
var builder = this.builder;
var componentElId = builder.memberExpression(
builder.identifier('__component'),
builder.identifier('elId'));
if (id.type === 'Literal' && id.value === '') {
let componentElId = builder.memberExpression(
builder.identifier('__component'),
builder.identifier('id'));
return builder.functionCall(componentElId, arguments.length === 0 ? [] : [ id ]);
return componentElId;
} else {
let componentElId = builder.memberExpression(
builder.identifier('__component'),
builder.identifier('elId'));
return builder.functionCall(componentElId, arguments.length === 0 ? [] : [ id ]);
}
}
getTransformHelper(el) {
@ -142,12 +122,12 @@ class TransformHelper {
}
TransformHelper.prototype.assignComponentId = require('./assignComponentId');
TransformHelper.prototype.convertToComponent = require('./convertToComponent');
TransformHelper.prototype.handleRootNodes = require('./handleRootNodes');
TransformHelper.prototype.handleIncludeNode = require('./handleIncludeNode');
TransformHelper.prototype.handleComponentEvents = require('./handleComponentEvents');
TransformHelper.prototype.handleComponentPreserve = require('./handleComponentPreserve');
TransformHelper.prototype.handleComponentPreserveAttrs = require('./handleComponentPreserveAttrs');
TransformHelper.prototype.handleComponentBind = require('./handleComponentBind');
TransformHelper.prototype.handleComponentKeyAttrs = require('./handleComponentKeyAttrs');
TransformHelper.prototype.handleScopedAttrs = require('./handleScopedAttrs');
TransformHelper.prototype.handleLegacyBind = require('./handleLegacyBind');
module.exports = TransformHelper;

View File

@ -23,10 +23,15 @@ module.exports = function transform(el, context) {
}
if (el.hasAttribute('w-body')) {
// This is a legacy code block and should be removed in Marko v5
context.deprecate('The "w-body" attribute is deprecated. Please use "<include(...)" instead. See: https://github.com/marko-js/marko/issues/492');
let builder = context.builder;
let bodyValue = el.getAttributeValue('w-body');
el.removeAttribute('w-body');
el.addAttribute({ // The old behavior is that the body content would be preserved if no new body content was provided
name: 'no-update-body-if',
argument: '!__component.b'//builder.negate(builder.memberExpression(builder.identifier('__component'), builder.identifier('b')))
});
let includeNode = context.createNodeForEl('include');
@ -44,11 +49,6 @@ module.exports = function transform(el, context) {
if (el.tagName === 'widget-types') {
context.setFlag('hasWidgetTypes');
} else if (el.tagName === 'include') {
transformHelper.handleComponentEvents();
transformHelper.handleIncludeNode(el);
transformHelper.getComponentArgs().compile(transformHelper);
return;
}
if (el.hasAttribute('w-el-id')) {
@ -56,9 +56,8 @@ module.exports = function transform(el, context) {
return;
}
if (el.isFlagSet('hasComponentBind') || el.hasAttribute('w-bind')) {
el.setFlag('hasComponentBind');
transformHelper.handleComponentBind();
if (el.hasAttribute('w-bind')) {
transformHelper.handleLegacyBind();
}
if (/* New preserve attributes */
@ -74,10 +73,11 @@ module.exports = function transform(el, context) {
transformHelper.handleComponentPreserve();
}
if (el.hasAttribute('key') || el.hasAttribute('ref') || el.hasAttribute('w-id')) {
if (!tagDefinitionHasOverridingKeyAttribute(el, context)) {
transformHelper.assignComponentId();
}
// Handle *:key properties (and deprecated w-for/for-key/for-ref)
transformHelper.handleScopedAttrs();
if ((el.hasAttribute('w-id') || el.hasAttribute('ref') || el.hasAttribute('key') || transformHelper.hasBoundComponentForTemplate()) && !tagDefinitionHasOverridingKeyAttribute(el, context)) {
transformHelper.assignComponentId();
}
if (el.hasAttribute('w-body')) {
@ -90,9 +90,6 @@ module.exports = function transform(el, context) {
// Handle w-on* properties
transformHelper.handleComponentEvents();
// Handle *:key properties (and deprecated w-for/for-key/for-ref)
transformHelper.handleComponentKeyAttrs();
// If we need to pass any information to a nested component then
// we start that information in the "out" so that it can be picked
// up later by the nested component. We call this "component args" and

View File

@ -5,13 +5,11 @@
* @return {ComponentDef} The ComponentDef instance
*/
module.exports = function getCurrentComponent(out) {
var componentsContext = out.data.___components;
var componentStack;
var len;
var componentsContext = out.___components;
if (!componentsContext || (len = (componentStack = componentsContext.___componentStack).length) < 2) {
if (!componentsContext) {
throw Error('No component found');
}
return componentStack[len - 1];
return componentsContext.___componentDef;
};

View File

@ -0,0 +1,9 @@
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
// var FLAG_HAS_BODY_EL = 2;
// var FLAG_HAS_HEAD_EL = 4;
module.exports = function markoKeyAttr(key, componentDef) {
if ((componentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER) === 0) {
return key + ' ' + componentDef.id;
}
};

View File

@ -1,19 +0,0 @@
var normalInclude = require('../../taglibs/core/include-tag').___doInclude;
var componentsUtil = require('../util');
var getElementById = componentsUtil.___getElementById;
var getComponentsContext = require('../ComponentsContext').___getComponentsContext;
module.exports = function include(input, out) {
if (!normalInclude(input, out)) {
var elId = input._elId;
// There's no body content so let's see if we should reuse
// the existing body content in the DOM
var existingEl = getElementById(out.___document, elId);
if (existingEl) {
var componentsContext = getComponentsContext(out);
componentsContext.___globalContext.___preserveDOMNode(elId, true /* body only */);
}
}
};

View File

@ -1,5 +0,0 @@
var normalInclude = require('../../taglibs/core/include-tag');
module.exports = function include(input, out) {
normalInclude(input, out);
};

View File

@ -9,7 +9,7 @@ var ComponentsContext = require('../ComponentsContext');
function handleAwaitBeforeRender(eventArgs) {
if (eventArgs.clientReorder) {
var asyncFragmentOut = eventArgs.out;
asyncFragmentOut.data.___components = new ComponentsContext(asyncFragmentOut, undefined, false);
asyncFragmentOut.___components = new ComponentsContext(asyncFragmentOut);
}
}

View File

@ -166,13 +166,13 @@
"no-output": true,
"@immediate": "boolean"
},
"<w-preserve>": {
"<_preserve>": {
"renderer": "./preserve-tag.js",
"@id": "string",
"@if": "expression",
"@body-only": "expression",
"autocomplete": [],
"deprecated": true
"@key": "string",
"@cid": "string",
"@if": "boolean",
"@body-only": "boolean",
"autocomplete": []
},
"<no-update>": {
"renderer": "./preserve-tag.js",

View File

@ -1,34 +1,46 @@
var getElementById = require('../util').___getElementById;
var componentsUtil = require('../util');
var componentLookup = componentsUtil.___componentLookup;
module.exports = function render(input, out) {
var globalComponentsContext = out.global.___components;
if (globalComponentsContext && globalComponentsContext.___isRerenderInBrowser !== true && globalComponentsContext.___rerenderComponent !== undefined) {
var id = input.id;
var componentsContext = out.___components;
if (componentsContext) {
// See if the DOM node with the given ID already exists.
// If so, then reuse the existing DOM node instead of re-rendering
// the children. We have to put a placeholder node that will get
// replaced out if we find that the DOM node has already been rendered
if (!('if' in input) || input['if']) {
var existingEl = getElementById(out.___document, id);
if (existingEl) {
var bodyOnly = input.bodyOnly === true;
// Don't actually render anything since the element is already in the DOM,
// but keep track that the node is being preserved so that we can ignore
// it while transforming the old DOM
var component = componentsContext.___componentDef.___component;
var globalComponentsContext = componentsContext.___globalContext;
var key = input.key;
var componentId;
if (!bodyOnly) {
var tagName = existingEl.tagName;
// If we are preserving the entire DOM node (not just the body)
// then that means that we have need to render a placeholder to
// mark the target location. We can then replace the placeholder
// node with the existing DOM node
out.element(tagName, { id: id });
if (key) {
if (component.___keyedElements[key]) {
var bodyOnly = input.bodyOnly === true;
// Don't actually render anything since the element is already in the DOM,
// but keep track that the node is being preserved so that we can ignore
// it while transforming the old DOM
if (bodyOnly) {
globalComponentsContext.___preservedElBodies[key] = true;
} else {
// If we are preserving the entire DOM node (not just the body)
// then that means that we have need to render a placeholder to
// mark the target location. We can then replace the placeholder
// node with the existing DOM node
out.element('', null, key, null, 0, 8 /* FLAG_PRESERVE */);
globalComponentsContext.___preservedEls[key] = true;
}
return;
}
} else if ((componentId = input.cid)) {
var existingComponent = componentLookup[componentId];
if (existingComponent) {
out.___preserveComponent(existingComponent);
globalComponentsContext.___renderedComponentsById[componentId] = true;
return;
}
globalComponentsContext.___preserveDOMNode(id, bodyOnly);
return;
}
}
}

View File

@ -1,12 +1,6 @@
var extend = require('raptor-util/extend');
var markoGlobal = extend(window.$MG, {
uid: 0
});
window.$MG = markoGlobal;
var runtimeId = markoGlobal.uid++;
var markoUID = window.$MUID || (window.$MUID = { i: 0 });
var runtimeId = markoUID.i++;
var componentLookup = {};
@ -17,18 +11,7 @@ function getComponentForEl(el, doc) {
if (el) {
var node = typeof el == 'string' ? (doc || defaultDocument).getElementById(el) : el;
if (node) {
var component = node._w;
while(component) {
var rootFor = component.___rootFor;
if (rootFor) {
component = rootFor;
} else {
break;
}
}
return component;
return node.___markoComponent;
}
}
}
@ -69,26 +52,26 @@ function emitLifecycleEvent(component, eventType, eventArg1, eventArg2) {
component.emit(eventType, eventArg1, eventArg2);
}
function destroyComponentForEl(el) {
var componentToDestroy = el._w;
function destroyComponentForNode(node) {
var componentToDestroy = node.___markoComponent;
if (componentToDestroy) {
componentToDestroy.___destroyShallow();
el._w = null;
while ((componentToDestroy = componentToDestroy.___rootFor)) {
componentToDestroy.___rootFor = null;
componentToDestroy.___destroyShallow();
}
}
}
function destroyElRecursive(el) {
var curChild = el.firstChild;
while(curChild) {
if (curChild.nodeType === 1) {
destroyComponentForEl(curChild);
destroyElRecursive(curChild);
function destroyNodeRecursive(node, component) {
if (node.nodeType === 1) {
var key;
if (component && (key = node.___markoKey)) {
delete component.___keyedElements[key];
}
var curChild = node.firstChild;
while(curChild) {
destroyComponentForNode(curChild);
destroyNodeRecursive(curChild, component);
curChild = curChild.nextSibling;
}
curChild = curChild.nextSibling;
}
}
@ -97,52 +80,36 @@ function nextComponentId() {
// marko runtimes. This allows multiple instances of marko to be
// loaded in the same window and they should all place nice
// together
return 'b' + ((markoGlobal.uid)++);
return 'b' + (markoUID.i++);
}
function nextComponentIdProvider(out) {
function nextComponentIdProvider() {
return nextComponentId;
}
function getElementById(doc, id) {
return doc.getElementById(id);
}
function attachBubblingEvent(componentDef, handlerMethodName, extraArgs) {
if (handlerMethodName) {
var id = componentDef.id;
var componentId = componentDef.id;
if (extraArgs) {
var isRerenderInBrowser = componentDef.___globalComponentsContext.___isRerenderInBrowser;
if (isRerenderInBrowser === true) {
// If we are bootstrapping a page rendered on the server
// we need to put the actual event args on the UI component
// since we will not actually be updating the DOM
var component = componentDef.___component;
var bubblingDomEvents = component.___bubblingDomEvents ||
( component.___bubblingDomEvents = [] );
bubblingDomEvents.push(extraArgs);
return;
} else {
return [handlerMethodName, id, extraArgs];
}
return [handlerMethodName, componentId, extraArgs];
} else {
return [handlerMethodName, id];
return [handlerMethodName, componentId];
}
}
}
function getMarkoPropsFromEl(el) {
var virtualProps = el._vprops;
if (virtualProps === undefined) {
virtualProps = el.getAttribute('data-marko');
if (virtualProps) {
virtualProps = JSON.parse(virtualProps);
var vElement = el.___markoVElement;
var virtualProps;
if (vElement) {
virtualProps = vElement.___properties;
} else {
virtualProps = el.___markoVProps;
if (!virtualProps) {
virtualProps = el.getAttribute('data-marko');
el.___markoVProps = virtualProps = virtualProps ? JSON.parse(virtualProps) : EMPTY_OBJECT;
}
el._vprops = virtualProps = virtualProps || EMPTY_OBJECT;
}
return virtualProps;
@ -152,9 +119,8 @@ exports.___runtimeId = runtimeId;
exports.___componentLookup = componentLookup;
exports.___getComponentForEl = getComponentForEl;
exports.___emitLifecycleEvent = emitLifecycleEvent;
exports.___destroyComponentForEl = destroyComponentForEl;
exports.___destroyElRecursive = destroyElRecursive;
exports.___destroyComponentForNode = destroyComponentForNode;
exports.___destroyNodeRecursive = destroyNodeRecursive;
exports.___nextComponentIdProvider = nextComponentIdProvider;
exports.___getElementById = getElementById;
exports.___attachBubblingEvent = attachBubblingEvent;
exports.___getMarkoPropsFromEl = getMarkoPropsFromEl;

View File

@ -1,3 +1,7 @@
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
// var FLAG_HAS_BODY_EL = 2;
// var FLAG_HAS_HEAD_EL = 4;
function nextComponentIdProvider(out) {
var prefix = out.global.componentIdPrefix || 's'; // "s" is for server (we use "b" for the browser)
var nextId = 0;
@ -21,7 +25,7 @@ function attachBubblingEvent(componentDef, handlerMethodName, extraArgs) {
// where the extra args will be found when the UI component is
// rerendered in the browser
if (componentDef.___willRerenderInBrowser === false) {
if (componentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER) {
if (eventIndex === 0) {
component.___bubblingDomEvents = [extraArgs];
} else {
@ -40,3 +44,5 @@ function attachBubblingEvent(componentDef, handlerMethodName, extraArgs) {
exports.___nextComponentIdProvider = nextComponentIdProvider;
exports.___isServer = true;
exports.___attachBubblingEvent = attachBubblingEvent;
exports.___destroyComponentForNode = function noop() {};
exports.___destroyNodeRecursive = function noop() {};

View File

@ -1,325 +1,539 @@
'use strict';
var defaultDoc = typeof document == 'undefined' ? undefined : document;
var specialElHandlers = require('./specialElHandlers');
var morphAttrs = require('../runtime/vdom/VElement').___morphAttrs;
var componentsUtil = require('../components/util');
var existingComponentLookup = componentsUtil.___componentLookup;
var destroyComponentForNode = componentsUtil.___destroyComponentForNode;
var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
var VElement = require('../runtime/vdom/vdom').___VElement;
var virtualizeElement = VElement.___virtualize;
var morphAttrs = VElement.___morphAttrs;
var eventDelegation = require('../components/event-delegation');
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
var COMPONENT_NODE = 2;
// var FLAG_IS_SVG = 1;
// var FLAG_IS_TEXTAREA = 2;
// var FLAG_SIMPLE_ATTRS = 4;
var FLAG_PRESERVE = 8;
function compareNodeNames(fromEl, toEl) {
return fromEl.nodeName === toEl.___nodeName;
return fromEl.___nodeName === toEl.___nodeName;
}
function onBeforeNodeDiscarded(node) {
return eventDelegation.___handleNodeDetach(node);
}
function getElementById(doc, id) {
return doc.getElementById(id);
function onNodeAdded(node, componentsContext) {
if (node.nodeType === 1) {
eventDelegation.___handleNodeAttach(node, componentsContext);
}
}
function insertBefore(node, referenceNode, parentNode) {
return parentNode.insertBefore(node, referenceNode);
}
function insertAfter(node, referenceNode, parentNode) {
return parentNode.insertBefore(node, referenceNode && referenceNode.nextSibling);
}
function morphdom(
fromNode,
parentNode,
startNode,
endNode,
toNode,
context,
onNodeAdded,
onBeforeElUpdated,
onBeforeNodeDiscarded,
onNodeDiscarded,
onBeforeElChildrenUpdated
doc,
componentsContext
) {
var globalComponentsContext;
var isRerenderInBrowser = false;
var doc = fromNode.ownerDocument || defaultDoc;
// This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
var removalList = [];
var foundKeys = {};
function walkDiscardedChildNodes(node) {
onNodeDiscarded(node);
var curChild = node.firstChild;
while (curChild) {
walkDiscardedChildNodes(curChild);
curChild = curChild.nextSibling;
}
if (componentsContext) {
globalComponentsContext = componentsContext.___globalContext;
isRerenderInBrowser = globalComponentsContext.___isRerenderInBrowser;
}
function createMarkerComment(referenceNode, parentNode) {
return doc.createComment('$marko');
}
function addVirtualNode(vEl, parentEl) {
var realEl = vEl.___actualize(doc);
function insertVirtualNodeBefore(vNode, key, referenceEl, parentEl, component, keySequence) {
var realNode = vNode.___actualize(doc);
insertBefore(realNode, referenceEl, parentEl);
if (parentEl) {
parentEl.appendChild(realEl);
}
onNodeAdded(realEl, context);
var vCurChild = vEl.___firstChild;
while (vCurChild) {
var realCurChild = null;
var key = vCurChild.id;
if (vNode.___nodeType === ELEMENT_NODE) {
if (key) {
var unmatchedFromEl = getElementById(doc, key);
if (unmatchedFromEl && compareNodeNames(vCurChild, unmatchedFromEl)) {
morphEl(unmatchedFromEl, vCurChild, false);
realEl.appendChild(realCurChild = unmatchedFromEl);
}
realNode.___markoKey = key;
(component = vNode.___component || component).___keyedElements[key] = realNode;
}
if (!realCurChild) {
addVirtualNode(vCurChild, realEl);
}
vCurChild = vCurChild.___nextSibling;
morphChildren(realNode, null, null, vNode, component, keySequence);
}
if (vEl.___nodeType === 1) {
var elHandler = specialElHandlers[vEl.nodeName];
if (elHandler !== undefined) {
elHandler(realEl, vEl);
}
}
return realEl;
onNodeAdded(realNode, componentsContext);
}
function morphEl(fromEl, toEl, childrenOnly) {
var toElKey = toEl.id;
var nodeName = toEl.___nodeName;
function insertVirtualComponentBefore(vComponent, referenceNode, referenceNodeParentEl) {
var component = vComponent.___component;
component.___startNode = component.___endNode = insertBefore(createMarkerComment(), referenceNode, referenceNodeParentEl);
morphComponent(referenceNodeParentEl, component, vComponent);
}
if (childrenOnly === false) {
if (toElKey) {
// If an element with an ID is being morphed then it is will be in the final
// DOM so clear it out of the saved elements collection
foundKeys[toElKey] = true;
function resolveComponentEndNode(startNode, vChild, parentNode) {
var endNode = startNode;
// We track text nodes because multiple adjacent VText nodes should
// be treated as a single VText node for purposes of pairing with HTML
// that was rendered on the server since browsers will only see
// a single text node
var isPrevText = vChild.___nodeType === TEXT_NODE;
while((vChild = vChild.___nextSibling)) {
var nextRealNode = endNode.nextSibling;
// We stop when there are no more corresponding real nodes or when
// we reach the end boundary for our UI component
if (!nextRealNode || nextRealNode.___endNode) {
break;
}
var isText = vChild.___nodeType === TEXT_NODE;
if (isText && isPrevText) {
// Pretend like we didn't see this VText node since it
// the previous vnode was also a VText node
continue;
}
endNode = nextRealNode;
isPrevText = isText;
}
var constId = toEl.___constId;
if (constId !== undefined) {
var otherProps = fromEl._vprops;
if (otherProps !== undefined && constId === otherProps.c) {
return;
return endNode;
}
function morphComponent(parentFromNode, component, vComponent) {
// We create a key sequence to generate unique keys since a key
// can be repeated
var keySequence = globalComponentsContext.___createKeySequence();
var startNode = component.___startNode;
var endNode = component.___endNode;
startNode.___markoComponent = undefined;
endNode.___endNode = undefined;
var beforeChild = startNode.previousSibling;
var afterChild = endNode.nextSibling;
morphChildren(parentFromNode, startNode, afterChild, vComponent, component, keySequence);
endNode = undefined;
if (beforeChild) {
startNode = beforeChild.nextSibling;
if (!startNode || startNode === afterChild) {
startNode = endNode = insertAfter(createMarkerComment(), beforeChild, parentFromNode);
}
} else {
startNode = parentFromNode.firstChild;
if (!startNode) {
startNode = endNode = insertAfter(createMarkerComment(), beforeChild, parentFromNode);
}
}
if (!endNode) {
if (afterChild) {
endNode = afterChild.previousSibling;
} else {
endNode = parentFromNode.lastChild;
}
}
// Make sure we don't use a detached node as the component boundary and
// we can't use a node that is already the boundary node for another component
if (startNode.___markoDetached !== undefined || startNode.___markoComponent) {
startNode = insertBefore(createMarkerComment(), startNode, parentFromNode);
}
if (endNode.___markoDetached !== undefined || endNode.___endNode) {
endNode = insertAfter(createMarkerComment(), endNode, parentFromNode);
}
startNode.___markoComponent = component;
endNode.___endNode = true;
component.___startNode = startNode;
component.___endNode = endNode;
return afterChild;
}
var detachedNodes = [];
function detachNode(node, parentNode, component) {
if (node.nodeType === ELEMENT_NODE) {
detachedNodes.push(node);
node.___markoDetached = component || true;
} else {
destroyNodeRecursive(node);
parentNode.removeChild(node);
}
}
function destroyComponent(component) {
component.destroy(onBeforeNodeDiscarded);
}
function morphChildren(parentFromNode, startNode, endNode, toNode, component, keySequence) {
var curFromNodeChild = startNode;
var curToNodeChild = toNode.___firstChild;
var curToNodeKey;
var curFromNodeKey;
var curToNodeType;
var fromNextSibling;
var toNextSibling;
var matchingFromEl;
var matchingFromComponent;
var toComponent;
var curVFromNodeChild;
var fromComponent;
outer: while (curToNodeChild) {
toNextSibling = curToNodeChild.___nextSibling;
curToNodeType = curToNodeChild.___nodeType;
if (curToNodeType === COMPONENT_NODE) {
toComponent = curToNodeChild.___component;
if ((matchingFromComponent = existingComponentLookup[toComponent.id]) === undefined) {
if (isRerenderInBrowser === true) {
var firstVChild = curToNodeChild.___firstChild;
if (firstVChild) {
if (!curFromNodeChild) {
curFromNodeChild = insertBefore(createMarkerComment(), null, parentFromNode);
}
toComponent.___startNode = curFromNodeChild;
toComponent.___endNode = resolveComponentEndNode(curFromNodeChild, firstVChild, parentFromNode);
} else {
toComponent.___startNode = toComponent.___endNode = insertBefore(createMarkerComment(), curFromNodeChild, parentFromNode);
}
curFromNodeChild = morphComponent(parentFromNode, toComponent, curToNodeChild);
} else {
insertVirtualComponentBefore(curToNodeChild, curFromNodeChild, parentFromNode);
}
} else {
if (matchingFromComponent.___startNode !== curFromNodeChild) {
if (curFromNodeChild &&
(fromComponent = curFromNodeChild.___markoComponent) &&
globalComponentsContext.___renderedComponentsById[fromComponent.id] === undefined) {
// The component associated with the current real DOM node was not rendered
// so we should just remove it out of the real DOM by destroying it
curFromNodeChild = fromComponent.___endNode.nextSibling;
destroyComponent(fromComponent);
continue;
}
// We need to move the existing component into
// the correct location
insertBefore(matchingFromComponent.___detach(), curFromNodeChild, parentFromNode);
}
if (curToNodeChild.___preserve) {
curFromNodeChild = matchingFromComponent.___endNode.nextSibling;
} else {
curFromNodeChild = morphComponent(parentFromNode, toComponent, curToNodeChild);
}
}
}
if (onBeforeElUpdated(fromEl, toElKey, context) === true) {
return;
}
curToNodeChild = toNextSibling;
continue;
} else if ((curToNodeKey = curToNodeChild.___key)) {
curVFromNodeChild = undefined;
curFromNodeKey = undefined;
morphAttrs(fromEl, toEl);
}
// We have a keyed element. This is the fast path for matching
// up elements
curToNodeKey = keySequence.___nextKey(curToNodeKey);
if (onBeforeElChildrenUpdated(fromEl, toElKey, context) === true) {
return;
}
if (curFromNodeChild) {
if (curFromNodeChild !== endNode) {
curFromNodeKey = curFromNodeChild.___markoKey;
curVFromNodeChild = curFromNodeChild.___markoVElement;
fromNextSibling = curFromNodeChild.nextSibling;
}
}
if (nodeName !== 'TEXTAREA') {
var curToNodeChild = toEl.___firstChild;
var curFromNodeChild = fromEl.firstChild;
var curToNodeKey;
var curFromNodeKey;
var fromNextSibling;
var toNextSibling;
var matchingFromEl;
outer: while (curToNodeChild) {
toNextSibling = curToNodeChild.___nextSibling;
curToNodeKey = curToNodeChild.id;
while (curFromNodeChild) {
fromNextSibling = curFromNodeChild.nextSibling;
curFromNodeKey = curFromNodeChild.id;
var curFromNodeType = curFromNodeChild.nodeType;
var isCompatible = undefined;
if (curFromNodeType === curToNodeChild.___nodeType) {
if (curFromNodeType === ELEMENT_NODE) {
// Both nodes being compared are Element nodes
if (curToNodeKey) {
// The target node has a key so we want to match it up with the correct element
// in the original DOM tree
if (curToNodeKey !== curFromNodeKey) {
// The current element in the original DOM tree does not have a matching key so
// let's check our lookup to see if there is a matching element in the original
// DOM tree
if ((matchingFromEl = getElementById(doc, curToNodeKey))) {
if (curFromNodeChild.nextSibling === matchingFromEl) {
// Special case for single element removals. To avoid removing the original
// DOM node out of the tree (since that can break CSS transitions, etc.),
// we will instead discard the current node and wait until the next
// iteration to properly match up the keyed target element with its matching
// element in the original tree
isCompatible = false;
} else {
// We found a matching keyed element somewhere in the original DOM tree.
// Let's moving the original DOM node into the current position and morph
// it.
// NOTE: We use insertBefore instead of replaceChild because we want to go through
// the `removeNode()` function for the node that is being discarded so that
// all lifecycle hooks are correctly invoked
if (curFromNodeKey === curToNodeKey) {
// Elements line up. Now we just have to make sure they are compatible
if ((curToNodeChild.___flags & FLAG_PRESERVE) === 0) {
// We just skip over the fromNode if it is preserved
fromEl.insertBefore(matchingFromEl, curFromNodeChild);
if (compareNodeNames(curToNodeChild, curVFromNodeChild)) {
morphEl(curFromNodeChild, curVFromNodeChild, curToNodeChild, component, keySequence);
} else {
// Remove the old node
detachNode(curFromNodeChild, parentFromNode, component);
fromNextSibling = curFromNodeChild.nextSibling;
removalList.push(curFromNodeChild);
curFromNodeChild = matchingFromEl;
}
} else {
// The nodes are not compatible since the "to" node has a key and there
// is no matching keyed node in the source tree
isCompatible = false;
}
}
} else if (curFromNodeKey) {
// The original has a key
isCompatible = false;
}
isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild) === true;
if (isCompatible === true) {
// We found compatible DOM elements so transform
// the current "from" node to match the current
// target DOM node.
morphEl(curFromNodeChild, curToNodeChild, false);
}
} else if (curFromNodeType === TEXT_NODE || curFromNodeType === COMMENT_NODE) {
// Both nodes being compared are Text or Comment nodes
isCompatible = true;
// Simply update nodeValue on the original node to
// change the text value
curFromNodeChild.nodeValue = curToNodeChild.___nodeValue;
// Incompatible nodes. Just move the target VNode into the DOM at this position
insertVirtualNodeBefore(curToNodeChild, curToNodeKey, curFromNodeChild, parentFromNode, component, keySequence);
}
}
if (isCompatible === true) {
// Advance both the "to" child and the "from" child since we found a match
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
// No compatible match so remove the old node from the DOM and continue trying to find a
// match in the original DOM. However, we only do this if the from node is not keyed
// since it is possible that a keyed node might match up with a node somewhere else in the
// target tree and we don't want to discard it just yet since it still might find a
// home in the final DOM tree. After everything is done we will remove any keyed nodes
// that didn't find a home
removalList.push(curFromNodeChild);
curFromNodeChild = fromNextSibling;
}
// If we got this far then we did not find a candidate match for
// our "to node" and we exhausted all of the children "from"
// nodes. Therefore, we will just append the current "to" node
// to the end
if (curToNodeKey && (matchingFromEl = getElementById(doc, curToNodeKey)) && compareNodeNames(matchingFromEl, curToNodeChild)) {
fromEl.appendChild(matchingFromEl);
morphEl(matchingFromEl, curToNodeChild, false);
} else {
addVirtualNode(curToNodeChild, fromEl);
if ((matchingFromEl = component.___keyedElements[curToNodeKey]) === undefined) {
if (isRerenderInBrowser === true && curFromNodeChild &&
curFromNodeChild.nodeType === ELEMENT_NODE &&
curFromNodeChild.nodeName === curToNodeChild.___nodeName) {
curVFromNodeChild = virtualizeElement(curFromNodeChild);
curFromNodeChild.___markoKey = curToNodeKey;
morphEl(curFromNodeChild, curVFromNodeChild, curToNodeChild, component, keySequence);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue;
}
insertVirtualNodeBefore(curToNodeChild, curToNodeKey, curFromNodeChild, parentFromNode, component, keySequence);
fromNextSibling = curFromNodeChild;
} else {
if (matchingFromEl.___markoDetached !== undefined) {
matchingFromEl.___markoDetached = undefined;
}
curVFromNodeChild = matchingFromEl.___markoVElement;
if (compareNodeNames(curVFromNodeChild, curToNodeChild)) {
if (fromNextSibling === matchingFromEl) {
// Single element removal:
// A <-> A
// B <-> C <-- We are here
// C D
// D
//
// Single element swap:
// A <-> A
// B <-> C <-- We are here
// C B
if (toNextSibling && toNextSibling.___key === curFromNodeKey) {
// Single element swap
// We want to stay on the current real DOM node
fromNextSibling = curFromNodeChild;
// But move the matching element into place
insertBefore(matchingFromEl, curFromNodeChild, parentFromNode);
} else {
// Single element removal
// We need to remove the current real DOM node
// and the matching real DOM node will fall into
// place. We will continue diffing with next sibling
// after the real DOM node that just fell into place
fromNextSibling = fromNextSibling.nextSibling;
if (curFromNodeChild) {
detachNode(curFromNodeChild, parentFromNode, component);
}
}
} else {
// A <-> A
// B <-> D <-- We are here
// C
// D
// We need to move the matching node into place
insertAfter(matchingFromEl, curFromNodeChild, parentFromNode);
if (curFromNodeChild) {
detachNode(curFromNodeChild, parentFromNode, component);
}
}
if ((curToNodeChild.___flags & FLAG_PRESERVE) === 0) {
morphEl(matchingFromEl, curVFromNodeChild, curToNodeChild, component, keySequence);
}
} else {
insertVirtualNodeBefore(curToNodeChild, curToNodeKey, curFromNodeChild, parentFromNode, component, keySequence);
detachNode(matchingFromEl, parentFromNode, component);
}
}
}
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue;
}
// We have processed all of the "to nodes". If curFromNodeChild is
// non-null then we still have some from nodes left over that need
// to be removed
while (curFromNodeChild) {
removalList.push(curFromNodeChild);
curFromNodeChild = curFromNodeChild.nextSibling;
// The know the target node is not a VComponent node and we know
// it is also not a preserve node. Let's now match up the HTML
// element, text node, comment, etc.
while (curFromNodeChild && curFromNodeChild !== endNode) {
if ((fromComponent = curFromNodeChild.___markoComponent) && fromComponent !== component) {
// The current "to" element is not associated with a component,
// but the current "from" element is associated with a component
// Even if we destroy the current component in the original
// DOM or not, we still need to skip over it since it is
// not compatible with the current "to" node
curFromNodeChild = fromComponent.___endNode.nextSibling;
if (!globalComponentsContext.___renderedComponentsById[fromComponent.id]) {
destroyComponent(fromComponent);
}
continue; // Move to the next "from" node
}
fromNextSibling = curFromNodeChild.nextSibling;
var curFromNodeType = curFromNodeChild.nodeType;
var isCompatible = undefined;
if (curFromNodeType === curToNodeType) {
if (curFromNodeType === ELEMENT_NODE) {
// Both nodes being compared are Element nodes
curVFromNodeChild = curFromNodeChild.___markoVElement;
if (curVFromNodeChild === undefined) {
if (isRerenderInBrowser === true) {
curVFromNodeChild = virtualizeElement(curFromNodeChild);
} else {
// Skip over nodes that don't look like ours...
curFromNodeChild = fromNextSibling;
continue;
}
} else if ((curFromNodeKey = curVFromNodeChild.___key)) {
// We have a keyed element here but our target VDOM node
// is not keyed so this not doesn't belong
isCompatible = false;
}
isCompatible = isCompatible !== false && compareNodeNames(curVFromNodeChild, curToNodeChild) === true;
if (isCompatible === true) {
// We found compatible DOM elements so transform
// the current "from" node to match the current
// target DOM node.
morphEl(curFromNodeChild, curVFromNodeChild, curToNodeChild, component, keySequence);
}
} else if (curFromNodeType === TEXT_NODE || curFromNodeType === COMMENT_NODE) {
// Both nodes being compared are Text or Comment nodes
isCompatible = true;
// Simply update nodeValue on the original node to
// change the text value
curFromNodeChild.nodeValue = curToNodeChild.___nodeValue;
}
}
if (isCompatible === true) {
// Advance both the "to" child and the "from" child since we found a match
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
if (curFromNodeKey) {
if (globalComponentsContext.___preservedEls[curFromNodeKey] === undefined) {
detachNode(curFromNodeChild, parentFromNode, component);
}
} else {
detachNode(curFromNodeChild, parentFromNode, component);
}
curFromNodeChild = fromNextSibling;
} // END: while (curFromNodeChild)
// If we got this far then we did not find a candidate match for
// our "to node" and we exhausted all of the children "from"
// nodes. Therefore, we will just append the current "to" node
// to the end
insertVirtualNodeBefore(curToNodeChild, curToNodeKey, curFromNodeChild, parentFromNode, component, keySequence);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
}
// We have processed all of the "to nodes". If curFromNodeChild is
// non-null then we still have some from nodes left over that need
// to be removed
while (curFromNodeChild && (endNode === null || curFromNodeChild !== endNode)) {
fromNextSibling = curFromNodeChild.nextSibling;
if ((fromComponent = curFromNodeChild.___markoComponent)) {
if (globalComponentsContext.___renderedComponentsById[fromComponent.id]) {
// Skip over this component since it was rendered in the target VDOM
// and will be moved into place later
curFromNodeChild = fromComponent.___endNode.nextSibling;
continue;
}
}
detachNode(curFromNodeChild, parentFromNode, component);
curFromNodeChild = fromNextSibling;
}
}
function morphEl(fromEl, vFromEl, toEl, component, keySequence) {
var toElKey = toEl.___key;
var nodeName = toEl.___nodeName;
component = toEl.___component || component;
if (isRerenderInBrowser === true && toElKey) {
component.___keyedElements[toElKey] = fromEl;
}
var constId = toEl.___constId;
if (constId !== undefined && vFromEl.___constId === constId) {
return;
}
morphAttrs(fromEl, vFromEl, toEl);
if (toElKey && globalComponentsContext.___preservedElBodies[toElKey] === true) {
// Don't morph the children since they are preserved
return;
}
if (nodeName !== 'TEXTAREA') {
morphChildren(fromEl, fromEl.firstChild, null, toEl, component, keySequence);
}
var specialElHandler = specialElHandlers[nodeName];
if (specialElHandler) {
if (specialElHandler !== undefined) {
specialElHandler(fromEl, toEl);
}
} // END: morphEl(...)
var morphedNode = fromNode;
var fromNodeType = morphedNode.nodeType;
var toNodeType = toNode.___nodeType;
var morphChildrenOnly = false;
var shouldMorphEl = true;
var newNode;
morphChildren(parentNode, startNode, endNode, toNode);
// Handle the case where we are given two DOM nodes that are not
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
if (fromNodeType == ELEMENT_NODE) {
if (toNodeType == ELEMENT_NODE) {
if (!compareNodeNames(fromNode, toNode)) {
newNode = toNode.___actualize(doc);
morphChildrenOnly = true;
removalList.push(fromNode);
}
} else {
// Going from an element node to a text or comment node
removalList.push(fromNode);
newNode = toNode.___actualize(doc);
shouldMorphEl = false;
}
} else if (fromNodeType == TEXT_NODE || fromNodeType == COMMENT_NODE) { // Text or comment node
if (toNodeType == fromNodeType) {
morphedNode.nodeValue = toNode.___nodeValue;
return morphedNode;
} else {
// Text node to something else
removalList.push(fromNode);
newNode = addVirtualNode(toNode);
shouldMorphEl = false;
}
}
detachedNodes.forEach(function(node) {
var detachedFromComponent = node.___markoDetached;
if (shouldMorphEl === true) {
morphEl(newNode || morphedNode, toNode, morphChildrenOnly);
}
if (detachedFromComponent !== undefined) {
node.___markoDetached = undefined;
if (newNode) {
if (fromNode.parentNode) {
fromNode.parentNode.replaceChild(newNode, fromNode);
}
}
destroyComponentForNode(node);
destroyNodeRecursive(node, detachedFromComponent !== true && detachedFromComponent);
// We now need to loop over any keyed nodes that might need to be
// removed. We only do the removal if we know that the keyed node
// never found a match. When a keyed node is matched up we remove
// it out of fromNodesLookup and we use fromNodesLookup to determine
// if a keyed node has been matched up or not
for (var i=0, len=removalList.length; i<len; i++) {
var node = removalList[i];
var key = node.id;
if (!key || foundKeys[key] === undefined) {
var parentNode = node.parentNode;
if (parentNode !== null || node === fromNode) {
if (onBeforeNodeDiscarded(node) == false) {
continue;
}
if (parentNode !== null) {
parentNode.removeChild(node);
}
walkDiscardedChildNodes(node);
if (onBeforeNodeDiscarded(node) != false) {
node.parentNode.removeChild(node);
}
}
}
return newNode || morphedNode;
});
}
module.exports = morphdom;

View File

@ -9,7 +9,9 @@ function syncBooleanAttrProp(fromEl, toEl, name) {
}
}
module.exports = {
// We use a JavaScript class to benefit from fast property lookup
function SpecialElHandlers() {}
SpecialElHandlers.prototype = {
/**
* Needed for IE. Apparently IE doesn't think that "selected" is an
* attribute when reading over the attributes using selectEl.attributes
@ -27,8 +29,8 @@ module.exports = {
syncBooleanAttrProp(fromEl, toEl, 'checked');
syncBooleanAttrProp(fromEl, toEl, 'disabled');
if (fromEl.value != toEl.value) {
fromEl.value = toEl.value;
if (fromEl.value != toEl.___value) {
fromEl.value = toEl.___value;
}
if (!toEl.___hasAttribute('value')) {
@ -37,7 +39,7 @@ module.exports = {
},
TEXTAREA: function(fromEl, toEl) {
var newValue = toEl.value;
var newValue = toEl.___value;
if (fromEl.value != newValue) {
fromEl.value = newValue;
}
@ -75,3 +77,5 @@ module.exports = {
}
}
};
module.exports = new SpecialElHandlers();

View File

@ -41,9 +41,9 @@ var proto = RenderResult.prototype = {
afterInsert: function(doc) {
var out = this.___out;
var globalComponentsContext = out.global.___components;
if (globalComponentsContext) {
this.___components = globalComponentsContext.___initComponents(doc);
var componentsContext = out.___components;
if (componentsContext) {
this.___components = componentsContext.___initComponents(doc);
} else {
this.___components = null;
}

View File

@ -1,7 +1,7 @@
var extend = require('raptor-util/extend');
var componentsUtil = require('../components/util');
var destroyComponentForEl = componentsUtil.___destroyComponentForEl;
var destroyElRecursive = componentsUtil.___destroyElRecursive;
var destroyComponentForNode = componentsUtil.___destroyComponentForNode;
var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
function resolveEl(el) {
if (typeof el == 'string') {
@ -15,8 +15,8 @@ function resolveEl(el) {
}
function beforeRemove(referenceEl) {
destroyElRecursive(referenceEl);
destroyComponentForEl(referenceEl);
destroyNodeRecursive(referenceEl);
destroyComponentForNode(referenceEl);
}
module.exports = function(target, getEl, afterInsert) {
@ -47,9 +47,7 @@ module.exports = function(target, getEl, afterInsert) {
var curChild = referenceEl.firstChild;
while(curChild) {
var nextSibling = curChild.nextSibling; // Just in case the DOM changes while removing
if (curChild.nodeType == 1) {
beforeRemove(curChild);
}
beforeRemove(curChild);
curChild = nextSibling;
}

View File

@ -62,6 +62,7 @@ function AsyncStream(global, writer, state, shouldBuffer) {
this._elStack = undefined; // Array
this.___componentArgs = null; // Component args
this.___components = null; // ComponentsContext
}
AsyncStream.DEFAULT_TIMEOUT = 10000;

View File

@ -4,13 +4,14 @@ var VElement = vdom.___VElement;
var VDocumentFragment = vdom.___VDocumentFragment;
var VComment = vdom.___VComment;
var VText = vdom.___VText;
var VComponent = vdom.___VComponent;
var virtualizeHTML = vdom.___virtualizeHTML;
var RenderResult = require('../RenderResult');
var defaultDocument = vdom.___defaultDocument;
var morphdom = require('../../morphdom');
var FLAG_FINISHED = 1;
var FLAG_LAST_FIRED = 2;
var EVENT_UPDATE = 'update';
var EVENT_FINISH = 'finish';
@ -41,6 +42,7 @@ function AsyncVDOMBuilder(globalData, parentNode, state) {
this.___stack = [parentNode];
this.___sync = false;
this.___vnode = undefined;
this.___components = null;
this.___componentArgs = null; // Component args
}
@ -48,39 +50,47 @@ var proto = AsyncVDOMBuilder.prototype = {
___isOut: true,
___document: defaultDocument,
___elementNode: function(element, childCount, pushToStack) {
var parent = this.___parent;
if (parent !== undefined) {
parent.___appendChild(element);
if (pushToStack === true) {
this.___stack.push(element);
this.___parent = element;
}
bc: function(component) {
var vComponent = new VComponent(component);
return this.___beginNode(vComponent, 0, true);
},
___preserveComponent: function(component) {
var vComponent = new VComponent(component, true);
this.___beginNode(vComponent, 0);
},
___beginNode: function(child, childCount, pushToStack) {
this.___parent.___appendChild(child);
if (pushToStack === true) {
this.___stack.push(child);
this.___parent = child;
}
return childCount === 0 ? this : element;
return childCount === 0 ? this : child;
},
element: function(tagName, attrs, childCount, flags, props) {
var element = new VElement(tagName, attrs, childCount, flags, props);
return this.___elementNode(element, childCount);
element: function(tagName, attrs, key, component, childCount, flags, props) {
var element = new VElement(tagName, attrs, key, component, childCount, flags, props);
return this.___beginNode(element, childCount);
},
___elementDynamicTag: function(tagName, attrs, childCount, flags, props) {
var element = VElement.___createElementDynamicTag(tagName, attrs, childCount, flags, props);
return this.___elementNode(element, childCount);
___elementDynamicTag: function(tagName, attrs, key, component, childCount, flags, props) {
var element = VElement.___createElementDynamicTag(tagName, attrs, key, component, childCount, flags, props);
return this.___beginNode(element, childCount);
},
n: function(node) {
n: function(node, component) {
// NOTE: We do a shallow clone since we assume the node is being reused
// and a node can only have one parent node.
return this.node(node.___cloneNode());
node = this.node(node.___cloneNode());
node.___component = component;
return node;
},
node: function(node) {
var parent = this.___parent;
if (parent !== undefined) {
parent.___appendChild(node);
}
this.___parent.___appendChild(node);
return this;
},
@ -99,15 +109,7 @@ var proto = AsyncVDOMBuilder.prototype = {
text = text.toString();
}
var parent = this.___parent;
if (parent !== undefined) {
var lastChild = parent.lastChild;
if (lastChild && lastChild.___Text) {
lastChild.___nodeValue += text;
} else {
parent.___appendChild(new VText(text));
}
}
this.___parent.___appendChild(new VText(text));
return this;
},
@ -124,15 +126,15 @@ var proto = AsyncVDOMBuilder.prototype = {
return this;
},
beginElement: function(tagName, attrs, childCount, flags, props) {
var element = new VElement(tagName, attrs, childCount, flags, props);
this.___elementNode(element, childCount, true);
beginElement: function(tagName, attrs, key, component, childCount, flags, props) {
var element = new VElement(tagName, attrs, key, component, childCount, flags, props);
this.___beginNode(element, childCount, true);
return this;
},
___beginElementDynamicTag: function(tagName, attrs, childCount, flags, props) {
var element = VElement.___createElementDynamicTag(tagName, attrs, childCount, flags, props);
this.___elementNode(element, childCount, true);
___beginElementDynamicTag: function(tagName, attrs, key, component, childCount, flags, props) {
var element = VElement.___createElementDynamicTag(tagName, attrs, key, component, childCount, flags, props);
this.___beginNode(element, childCount, true);
return this;
},
@ -304,23 +306,30 @@ var proto = AsyncVDOMBuilder.prototype = {
var node = this.___vnode;
if (!node) {
var vdomTree = this.___getOutput();
node = this.___vnode = vdomTree.actualize(doc || this.___document || document);
// Create the root document fragment node
doc = doc || this.___document || document;
this.___vnode = node = vdomTree.___actualize(doc);
morphdom(node, null, null, vdomTree, doc, this.___components);
}
return node;
},
toString: function() {
var docFragment = this.___getNode();
toString: function(doc) {
var docFragment = this.___getNode(doc);
var html = '';
if (docFragment.hasChildNodes()) {
var children = docFragment.childNodes;
for (var i = 0; i < children.length; i++) {
var child = children[i];
// get outerHTML if exists, otherwise default to nodeValue
html += child.outerHTML || child.nodeValue;
var child = docFragment.firstChild;
while(child) {
var nextSibling = child.nextSibling;
if (child.nodeType != 1) {
var container = docFragment.ownerDocument.createElement('div');
container.appendChild(child.cloneNode());
html += container.innerHTML;
} else {
html += child.outerHTML;
}
child = nextSibling;
}
return html;

View File

@ -10,7 +10,8 @@ VComment.prototype = {
___nodeType: 8,
___actualize: function(doc) {
return doc.createComment(this.___nodeValue);
var nodeValue = this.___nodeValue;
return doc.createComment(nodeValue);
},
___cloneNode: function() {

View File

@ -0,0 +1,16 @@
var VNode = require('./VNode');
var inherit = require('raptor-util/inherit');
function VComponent(component, preserve) {
this.___VNode(null /* childCount */);
this.___component = component;
this.___preserve = preserve;
}
VComponent.prototype = {
___nodeType: 2
};
inherit(VComponent, VNode);
module.exports = VComponent;

View File

@ -8,8 +8,9 @@ function VDocumentFragmentClone(other) {
this.___nextSiblingInternal = null;
}
function VDocumentFragment(documentFragment) {
function VDocumentFragment(out) {
this.___VNode(null /* childCount */);
this.___out = out;
}
VDocumentFragment.prototype = {

View File

@ -1,13 +1,16 @@
/* jshint newcap:false */
var VNode = require('./VNode');
var inherit = require('raptor-util/inherit');
var NS_XLINK = 'http://www.w3.org/1999/xlink';
var ATTR_XLINK_HREF = 'xlink:href';
var xmlnsRegExp = /^xmlns(:|$)/;
var toString = String;
var FLAG_IS_SVG = 1;
var FLAG_IS_TEXTAREA = 2;
var FLAG_SIMPLE_ATTRS = 4;
// var FLAG_PRESERVE = 8;
var defineProperty = Object.defineProperty;
@ -45,41 +48,49 @@ function VElementClone(other) {
this.___parentNode = null;
this.___nextSiblingInternal = null;
this.___key = other.___key;
this.___attributes = other.___attributes;
this.___properties = other.___properties;
this.___namespaceURI = other.___namespaceURI;
this.___nodeName = other.___nodeName;
this.___flags = other.___flags;
this.___value = other.___value;
this.___valueInternal = other.___valueInternal;
this.___constId = other.___constId;
this.___isTextArea = other.___isTextArea;
}
function VElement(tagName, attrs, childCount, flags, props) {
function VElement(tagName, attrs, key, component, childCount, flags, props) {
this.___VNode(childCount);
var constId, namespaceURI;
var constId;
var namespaceURI;
var isTextArea;
if (props) {
constId = props.c;
constId = props.i;
}
if ((this.___flags = flags || 0)) {
if (flags & FLAG_IS_SVG) {
namespaceURI = 'http://www.w3.org/2000/svg';
}
if (flags & FLAG_IS_TEXTAREA) {
isTextArea = true;
}
}
this.___key = key;
this.___component = component;
this.___attributes = attrs || EMPTY_OBJECT;
this.___properties = props || EMPTY_OBJECT;
this.___namespaceURI = namespaceURI;
this.___nodeName = tagName;
this.___value = null;
this.___valueInternal = null;
this.___constId = constId;
this.___isTextArea = isTextArea;
}
VElement.prototype = {
___VElement: true,
___nodeType: 1,
___cloneNode: function() {
@ -93,8 +104,8 @@ VElement.prototype = {
* @param {int|null} attrCount The number of attributes (or `null` if not known)
* @param {int|null} childCount The number of child nodes (or `null` if not known)
*/
e: function(tagName, attrs, childCount, flags, props) {
var child = this.___appendChild(new VElement(tagName, attrs, childCount, flags, props));
e: function(tagName, attrs, key, component, childCount, flags, props) {
var child = this.___appendChild(new VElement(tagName, attrs, key, component, childCount, flags, props));
if (childCount === 0) {
return this.___finishChild();
@ -110,8 +121,8 @@ VElement.prototype = {
* @param {int|null} attrCount The number of attributes (or `null` if not known)
* @param {int|null} childCount The number of child nodes (or `null` if not known)
*/
ed: function(tagName, attrs, childCount, flags, props) {
var child = this.___appendChild(VElement.___createElementDynamicTag(tagName, attrs, childCount, flags, props));
ed: function(tagName, attrs, key, component, childCount, flags, props) {
var child = this.___appendChild(VElement.___createElementDynamicTag(tagName, attrs, key, component, childCount, flags, props));
if (childCount === 0) {
return this.___finishChild();
@ -126,8 +137,10 @@ VElement.prototype = {
*
* @param {String} value The value for the new Comment node
*/
n: function(node) {
this.___appendChild(node.___cloneNode());
n: function(node, component) {
node = node.___cloneNode();
node.___component = component;
this.___appendChild(node);
return this.___finishChild();
},
@ -163,12 +176,10 @@ VElement.prototype = {
}
if (flags & FLAG_IS_TEXTAREA) {
el.value = this.___value;
el.value = this.___valueInternal;
}
el._vattrs = attributes;
el._vprops = this.___properties;
el._vflags = flags;
el.___markoVElement = this;
return el;
},
@ -179,7 +190,7 @@ VElement.prototype = {
// different namespaces
var value = this.___attributes[name];
return value != null && value !== false;
},
}
};
inherit(VElement, VNode);
@ -195,15 +206,9 @@ var proto = VElementClone.prototype = VElement.prototype;
});
});
defineProperty(proto, 'id', {
defineProperty(proto, '___value', {
get: function () {
return this.___attributes.id;
}
});
defineProperty(proto, 'value', {
get: function () {
var value = this.___value;
var value = this.___valueInternal;
if (value == null) {
value = this.___attributes.value;
}
@ -211,16 +216,10 @@ defineProperty(proto, 'value', {
}
});
defineProperty(proto, '___isTextArea', {
get: function () {
return this.___flags & FLAG_IS_TEXTAREA;
}
});
VElement.___createElementDynamicTag = function(tagName, attrs, childCount, flags, props) {
VElement.___createElementDynamicTag = function(tagName, attrs, key, component, childCount, flags, props) {
var namespace = attrs && attrs.xmlns;
tagName = namespace ? tagName : tagName.toUpperCase();
var element = new VElement(tagName, attrs, childCount, flags, props);
var element = new VElement(tagName, attrs, key, component, childCount, flags, props);
element.___namespaceURI = namespace;
return element;
};
@ -233,15 +232,64 @@ VElement.___removePreservedAttributes = function(attrs) {
return attrs;
};
VElement.___morphAttrs = function(fromEl, toEl) {
function virtualizeElement(node, virtualizeChildNodes) {
var attributes = node.attributes;
var attrCount = attributes.length;
var attrs;
if (attrCount) {
attrs = {};
for (var i=0; i<attrCount; i++) {
var attr = attributes[i];
var attrName = attr.name;
if (!xmlnsRegExp.test(attrName) && attrName !== 'data-marko') {
var attrNamespaceURI = attr.namespaceURI;
if (attrNamespaceURI === NS_XLINK) {
attrs[ATTR_XLINK_HREF] = attr.value;
} else {
attrs[attrName] = attr.value;
}
}
}
}
var flags = 0;
var tagName = node.nodeName;
if (tagName === 'TEXTAREA') {
flags |= FLAG_IS_TEXTAREA;
}
var vdomEl = new VElement(tagName, attrs, null /*key*/, null /*component*/, 0 /*child count*/, flags, null /*props*/);
if (node.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
vdomEl.___namespaceURI = node.namespaceURI;
}
if (vdomEl.___isTextArea) {
vdomEl.___valueInternal = node.value;
} else {
if (virtualizeChildNodes) {
virtualizeChildNodes(node, vdomEl);
}
}
return vdomEl;
}
VElement.___virtualize = virtualizeElement;
VElement.___morphAttrs = function(fromEl, vFromEl, toEl) {
var removePreservedAttributes = VElement.___removePreservedAttributes;
var fromFlags = vFromEl.___flags;
fromEl.___markoVElement = toEl;
var attrs = toEl.___attributes;
var props = fromEl._vprops = toEl.___properties;
var props = toEl.___properties;
var attrName;
var i;
// We use expando properties to associate the previous HTML
// attributes provided as part of the VDOM node with the
@ -251,52 +299,26 @@ VElement.___morphAttrs = function(fromEl, toEl) {
// real VElement node will not have the expando property
// so we build the attribute map from the expando property
var oldAttrs = fromEl._vattrs;
var oldAttrs = vFromEl.___attributes;
if (oldAttrs) {
if (oldAttrs == attrs) {
if (oldAttrs === attrs) {
// For constant attributes the same object will be provided
// every render and we can use that to our advantage to
// not waste time diffing a constant, immutable attribute
// map.
return;
} else {
oldAttrs = removePreservedAttributes(oldAttrs, props, true);
oldAttrs = removePreservedAttributes(oldAttrs, props);
}
} else {
// We need to build the attribute map from the real attributes
oldAttrs = {};
var oldAttributesList = fromEl.attributes;
for (i = oldAttributesList.length - 1; i >= 0; --i) {
var attr = oldAttributesList[i];
if (attr.specified !== false) {
attrName = attr.name;
if (attrName !== 'data-marko') {
var attrNamespaceURI = attr.namespaceURI;
if (attrNamespaceURI === NS_XLINK) {
oldAttrs[ATTR_XLINK_HREF] = attr.value;
} else {
oldAttrs[attrName] = attr.value;
}
}
}
}
// We don't want preserved attributes to show up in either the old
// or new attribute map.
removePreservedAttributes(oldAttrs, props, false);
}
fromEl._vattrs = attrs;
var attrValue;
var flags = toEl.___flags;
var oldFlags;
var toFlags = toEl.___flags;
if (flags & FLAG_SIMPLE_ATTRS && ((oldFlags = fromEl._vflags) & FLAG_SIMPLE_ATTRS)) {
if (toFlags & FLAG_SIMPLE_ATTRS && fromFlags & FLAG_SIMPLE_ATTRS) {
if (oldAttrs['class'] !== (attrValue = attrs['class'])) {
fromEl.className = attrValue;
}
@ -309,6 +331,7 @@ VElement.___morphAttrs = function(fromEl, toEl) {
return;
}
// In some cases we only want to set an attribute value for the first
// render or we don't want certain attributes to be touched. To support
// that use case we delete out all of the preserved attributes
@ -352,7 +375,7 @@ VElement.___morphAttrs = function(fromEl, toEl) {
// was not a virtualized node (i.e., a node that was not rendered by a
// Marko template, but rather a node that was created from an HTML
// string or a real DOM node).
if (!attrs.id || props.___virtualized === true) {
if (toEl.___key === null) {
for (attrName in oldAttrs) {
if (!(attrName in attrs)) {
if (attrName === ATTR_XLINK_HREF) {

View File

@ -1,6 +1,4 @@
/* jshint newcap:false */
var specialElHandlers = require('../../morphdom/specialElHandlers');
function VNode() {}
VNode.prototype = {
@ -13,6 +11,8 @@ VNode.prototype = {
this.___nextSiblingInternal = null;
},
___component: null,
get ___firstChild() {
var firstChild = this.___firstChildInternal;
@ -49,10 +49,10 @@ VNode.prototype = {
___appendChild: function(child) {
this.___childCount++;
if (this.___isTextArea) {
if (this.___isTextArea === true) {
if (child.___Text) {
var childValue = child.___nodeValue;
this.___value = (this.___value || '') + childValue;
this.___valueInternal = (this.___valueInternal || '') + childValue;
} else {
throw TypeError();
}
@ -74,32 +74,13 @@ VNode.prototype = {
},
___finishChild: function finishChild() {
if (this.___childCount == this.___finalChildCount && this.___parentNode) {
if (this.___childCount === this.___finalChildCount && this.___parentNode) {
return this.___parentNode.___finishChild();
} else {
return this;
}
},
actualize: function(doc) {
var actualNode = this.___actualize(doc);
var curChild = this.___firstChild;
while(curChild) {
actualNode.appendChild(curChild.actualize(doc));
curChild = curChild.___nextSibling;
}
if (this.___nodeType === 1) {
var elHandler = specialElHandlers[this.___nodeName];
if (elHandler !== undefined) {
elHandler(actualNode, this);
}
}
return actualNode;
}
// ,toJSON: function() {
// var clone = Object.assign({

View File

@ -10,8 +10,8 @@ var extend = require('raptor-util/extend');
var classList = commonHelpers.cl;
var helpers = extend({
e: function(tagName, attrs, childCount, flags, props) {
return new VElement(tagName, attrs, childCount, flags, props);
e: function(tagName, attrs, key, component, childCount, flags, props) {
return new VElement(tagName, attrs, key, component, childCount, flags, props);
},
t: function(value) {

View File

@ -1,11 +1,9 @@
var extend = require('raptor-util/extend');
function removePreservedAttributes(attrs, props, clone) {
function removePreservedAttributes(attrs, props) {
var preservedAttrs = props && props.noupdate;
if (preservedAttrs) {
if (clone) {
attrs = extend({}, attrs);
}
attrs = extend({}, attrs);
preservedAttrs.forEach(function(preservedAttrName) {
delete attrs[preservedAttrName];
});

View File

@ -3,12 +3,11 @@ var VComment = require('./VComment');
var VDocumentFragment = require('./VDocumentFragment');
var VElement = require('./VElement');
var VText = require('./VText');
var VComponent = require('./VComponent');
var FLAG_IS_TEXTAREA = 2;
var defaultDocument = typeof document != 'undefined' && document;
var specialHtmlRegexp = /[&<]/;
var xmlnsRegExp = /^xmlns(:|$)/;
var virtualizedProps = { ___virtualized: true };
function virtualizeChildNodes(node, vdomParent) {
var curChild = node.firstChild;
@ -18,44 +17,10 @@ function virtualizeChildNodes(node, vdomParent) {
}
}
function virtualize(node) {
function virtualize(node, shallow) {
switch(node.nodeType) {
case 1:
var attributes = node.attributes;
var attrCount = attributes.length;
var attrs;
if (attrCount) {
attrs = {};
for (var i=0; i<attrCount; i++) {
var attr = attributes[i];
var attrName = attr.name;
if (!xmlnsRegExp.test(attrName)) {
attrs[attrName] = attr.value;
}
}
}
var flags = 0;
var tagName = node.nodeName;
if (tagName === 'TEXTAREA') {
flags |= FLAG_IS_TEXTAREA;
}
var vdomEl = new VElement(tagName, attrs, null, flags, virtualizedProps);
if (node.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
vdomEl.___namespaceURI = node.namespaceURI;
}
if (vdomEl.___isTextArea) {
vdomEl.___value = node.value;
} else {
virtualizeChildNodes(node, vdomEl);
}
return vdomEl;
return VElement.___virtualize(node, virtualizeChildNodes);
case 3:
return new VText(node.nodeValue);
case 8:
@ -126,6 +91,7 @@ exports.___VComment = VComment;
exports.___VDocumentFragment = VDocumentFragment;
exports.___VElement = VElement;
exports.___VText = VText;
exports.___VComponent = VComponent;
exports.___virtualize = virtualize;
exports.___virtualizeHTML = virtualizeHTML;
exports.___defaultDocument = defaultDocument;

View File

@ -76,6 +76,7 @@
},
"<import>": {
"code-generator": "./import-tag",
"no-output": true,
"parse-options": {
"relaxRequireCommas": true
}
@ -135,7 +136,8 @@
{
"descriptionMoreURL": "http://markojs.com/docs/marko/language-guide/#macros"
}
]
],
"no-output": true
},
"<macro-body>": {
"code-generator": "./macro-body-tag",
@ -167,6 +169,7 @@
},
"<static>": {
"code-generator": "./static-tag",
"no-output": true,
"parse-options": {
"ignoreAttributes": true
}

View File

@ -1127,7 +1127,8 @@
"html": true,
"attribute-groups": [
"html-attributes"
]
],
"openTagOnly": true
},
"attribute-groups": {
"html-attributes": {

View File

@ -1 +1 @@
<html><head><title>Welcome Frank</title></head><body><div id="s0">Hello</div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0",0,{},{"_":1}]],"t":["/marko-test$1.0.0/autotests/async-render/components-await-title/components/hello/index.marko"]})||w.$components})()</script></body></html>
<html><head><title>Welcome Frank</title></head><body><!--M#s0--><div>Hello</div><!--M$s0--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0",0,{},{"f":1}]],"t":["/marko-test$1.0.0/autotests/async-render/components-await-title/components/hello/index.marko"]})||w.$components})()</script></body></html>

View File

@ -5,7 +5,7 @@ var marko_template = module.exports = require("marko/src/vdom").t();
function render(input, out) {
var data = input;
out.ed(foo ? "foo" : "bar", null, 0);
out.ed(foo ? "foo" : "bar", null, null, null, 0);
}
marko_template._ = render;

View File

@ -6,12 +6,12 @@ var marko_template = module.exports = require("marko/src/vdom").t(),
marko_createElement = marko_helpers.e,
marko_const = marko_helpers.const,
marko_const_nextId = marko_const("295cea"),
marko_node0 = marko_createElement("DIV", null, 1, 0, {
c: marko_const_nextId()
marko_node0 = marko_createElement("DIV", null, null, null, 1, 0, {
i: marko_const_nextId()
})
.t("No colors!"),
marko_node1 = marko_createElement("DIV", null, 1, 0, {
c: marko_const_nextId()
marko_node1 = marko_createElement("DIV", null, null, null, 1, 0, {
i: marko_const_nextId()
})
.t("No colors!");
@ -28,7 +28,7 @@ function render(input, out) {
out.be("UL");
marko_forEach(input.colors, function(color) {
out.e("LI", null, 1)
out.e("LI", null, null, null, 1)
.t(color);
});
@ -41,7 +41,7 @@ function render(input, out) {
out.be("UL");
marko_forEach(input.colors, function(color) {
out.e("LI", null, 1)
out.e("LI", null, null, null, 1)
.t(color);
});

View File

@ -8,13 +8,13 @@ var marko_template = module.exports = require("marko/src/vdom").t(),
marko_node0 = marko_createElement("svg", {
width: "140",
height: "30"
}, 1, 1, {
c: marko_const_nextId()
}, null, null, 1, 1, {
i: marko_const_nextId()
})
.e("a", {
"xlink:href": "https://developer.mozilla.org/en-US/docs/SVG",
target: "_blank"
}, 0, 1);
}, null, null, 0, 1);
function render(input, out) {
var data = input;

View File

@ -15,8 +15,8 @@ function render(input, out) {
var isCircle = true;
out.e("svg", marko_attrs0, 1, 1)
.e(isCircle ? "circle" : "square", marko_attrs1, 0, 1);
out.e("svg", marko_attrs0, null, null, 1, 1)
.e(isCircle ? "circle" : "square", marko_attrs1, null, null, 0, 1);
}
marko_template._ = render;

View File

@ -8,14 +8,14 @@ var marko_template = module.exports = require("marko/src/vdom").t(),
marko_node0 = marko_createElement("svg", {
viewBox: "0 0 200 200",
xmlns: "http://www.w3.org/2000/svg"
}, 1, 1, {
c: marko_const_nextId()
}, null, null, 1, 1, {
i: marko_const_nextId()
})
.e("circle", {
cx: "100",
cy: "100",
r: "100"
}, 0, 1);
}, null, null, 0, 1);
function render(input, out) {
var data = input;

View File

@ -2,11 +2,11 @@ class {
}
<div>
<div key="root">
<for(index, field in input.fields)>
<div>
<input key="field[${index}]" type="text" value=field.value/>
<label for-key="field[${index}]">${field.label}</label>
</div>
</for>
</div>
</div>

View File

@ -0,0 +1,31 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var fields = [
{
value: 'name',
label: 'Name'
},
{
value: 'age',
label: 'Age'
}
];
var component = helpers.mount(require('./index'), {
fields: fields
});
var inputs = component.getEl('root').querySelectorAll('input');
var labels = component.getEl('root').querySelectorAll('label');
expect(inputs.length).to.equal(fields.length);
for (var i=0; i<fields.length; i++) {
var input = inputs[i];
var label = labels[i];
expect(label.getAttribute('for')).to.equal(input.id);
expect(input.value).to.equal(fields[i].value);
expect(label.innerHTML).to.equal(fields[i].label);
}
};

View File

@ -14,7 +14,12 @@ module.exports = function(helpers) {
]
});
var inputs = widget.getEls('field');
var inputs = [];
for (var i=0; i<2; i++) {
inputs.push(widget.getEl('field[' + i + ']'));
}
expect(inputs.length).to.equal(2);
expect(inputs[0].value).to.equal('name');
};

View File

@ -3,7 +3,6 @@ var expect = require('chai').expect;
module.exports = function(helpers, done) {
var widget = helpers.mount(require('./index'), {});
expect(widget.$().attr('id')).to.equal(widget.id);
expect(widget.$().attr('class')).to.equal('app-jquery-proxy');
expect(widget.$('#foo').html()).to.equal('foo');
expect(widget.$('#fooText').html()).to.equal('fooText');

View File

@ -11,5 +11,5 @@ module.exports = function(helpers) {
label: 'Bar'
});
expect(widget.el.id).to.equal(oldId);
};
expect(widget.id).to.equal(oldId);
};

View File

@ -7,7 +7,7 @@ module.exports = function(helpers) {
version: 0
});
expect(window.rerenderInitOrder).to.deep.equal(['childA', 'childB', 'parent']);
expect(window.rerenderInitOrder).to.deep.equal(['childB', 'childA', 'parent']);
window.rerenderInitOrder = [];
@ -15,7 +15,7 @@ module.exports = function(helpers) {
widget.update();
// console.log('ACTUAL ORDER: ', window.rerenderInitOrder);
expect(window.rerenderInitOrder).to.deep.equal(['childA', 'childB', 'parent']);
expect(window.rerenderInitOrder).to.deep.equal(['childB', 'childA', 'parent']);
delete window.rerenderInitOrder;
};
};

View File

@ -6,7 +6,7 @@ module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {});
var oldButton1Widget = widget.getWidget('button1');
var oldButton2Widget = widget.getEl('button2').__widget;
var oldButton2Widget = widget.getWidget('button2');
var oldButton1El = oldButton1Widget.el;
var oldButton2El = widget.getEl('button2');
@ -22,7 +22,7 @@ module.exports = function(helpers) {
// // Both button widgets should be reused
expect(widget.getWidget('button1')).to.equal(oldButton1Widget);
expect(widget.getEl('button2').__widget).to.equal(oldButton2Widget);
expect(widget.getWidget('button2')).to.equal(oldButton2Widget);
expect(widget.getWidget('button1').el.innerHTML).to.equal('small');
@ -35,4 +35,4 @@ module.exports = function(helpers) {
//
// // State didn't change for button2 so it should be the same el
expect(newButton2El).to.equal(oldButton2El);
};
};

View File

@ -4,7 +4,7 @@ module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {});
var oldButton1Widget = widget.getWidget('button1');
var oldButton2Widget = widget.getEl('button2').__widget;
var oldButton2Widget = widget.getWidget('button2');
var oldButton1El = oldButton1Widget.el;
var oldButton2El = widget.getEl('button2');
@ -20,7 +20,7 @@ module.exports = function(helpers) {
// // Both button widgets should be reused
expect(widget.getWidget('button1')).to.equal(oldButton1Widget);
expect(widget.getEl('button2').__widget).to.equal(oldButton2Widget);
expect(widget.getWidget('button2')).to.equal(oldButton2Widget);
expect(widget.getWidget('button1').el.innerHTML).to.equal('small');
@ -33,4 +33,4 @@ module.exports = function(helpers) {
//
// // State didn't change for button2 so it should be the same el
expect(newButton2El).to.equal(oldButton2El);
};
};

View File

@ -0,0 +1,3 @@
class {}
<div.inner>${input.count}</div>

View File

@ -0,0 +1,9 @@
class {
onCreate() {
this.state = {
count: 0
}
}
}
<inner count=state.count key="inner"/>

View File

@ -0,0 +1,21 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var component = helpers.mount(require('./index'), { });
var innerComponent = component.getComponent('inner');
expect(innerComponent.___startNode.className).to.equal('inner');
expect(component.___startNode).to.not.equal(innerComponent.___startNode);
expect(component.___endNode).to.not.equal(innerComponent.___endNode);
expect(helpers.targetEl.querySelector('.inner').innerHTML).to.equal('0');
component.state.count++;
component.update();
innerComponent = component.getComponent('inner');
expect(innerComponent.___startNode.className).to.equal('inner');
expect(component.___startNode).to.not.equal(innerComponent.___startNode);
expect(component.___endNode).to.not.equal(innerComponent.___endNode);
expect(helpers.targetEl.querySelector('.inner').innerHTML).to.equal('1');
};

View File

@ -0,0 +1,3 @@
class {}
<div.bar>${input.name}</div>

View File

@ -0,0 +1,3 @@
class {}
<div.foo>${input.name}</div>

View File

@ -0,0 +1,19 @@
class {
onCreate() {
this.state = {
count: 0
}
}
}
<div.root key="root">
<if(state.count === 0)>
<foo key="a" name="foo-a" count=state.count/>
<bar key="b" name="bar-b" count=state.count/>
</if>
<else>
<foo key="a" name="foo-a" count=state.count/>
<foo key="b" name="foo-b" count=state.count/>
<bar key="c" name="bar-c" count=state.count/>
</else>
</div>

View File

@ -0,0 +1,21 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var component = helpers.mount(require('./index'), { });
var children;
children = component.getEl('root').children;
expect(children.length).to.equal(2);
expect(children[0].innerHTML).to.equal('foo-a');
expect(children[1].innerHTML).to.equal('bar-b');
component.state.count++;
component.update();
children = component.getEl('root').children;
expect(children.length).to.equal(3);
expect(children[0].innerHTML).to.equal('foo-a');
expect(children[1].innerHTML).to.equal('foo-b');
expect(children[2].innerHTML).to.equal('bar-c');
};

View File

@ -0,0 +1,3 @@
class {}
<div.bar>bar ${component.id}</div>

View File

@ -0,0 +1,3 @@
class {}
<div.foo>foo ${component.id}</div>

View File

@ -0,0 +1,19 @@
class {
onCreate() {
this.state = {
count: 0
}
}
}
<div.root key="root">
<if(state.count === 0)>
<foo key="a" count=state.count/>
<bar key="b" count=state.count/>
<foo key="c" count=state.count/>
</if>
<else>
<foo key="a" count=state.count/>
<foo key="b" count=state.count/>
</else>
</div>

View File

@ -0,0 +1,16 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var component = helpers.mount(require('./index'), { });
var children;
children = component.getEl('root').children;
expect(children.length).to.equal(3);
component.state.count++;
component.update();
children = component.getEl('root').children;
expect(children.length).to.equal(2);
};

View File

@ -0,0 +1,16 @@
class {
onCreate() {
this.state = {
count: 1
}
}
increment() {
this.state.count++;
}
}
<for(var i=0; i<state.count; i++)>
<div>${i}</div>
</for>

View File

@ -0,0 +1,3 @@
class {}
<span.foo>foo ${component.id}</span>

View File

@ -0,0 +1,12 @@
class {
incrementBar() {
this.getComponent('bar').increment();
this.getComponent('bar').update();
}
}
<div.root key="root">
<foo/>
<bar key="bar"/>
<foo/>
</div>

View File

@ -0,0 +1,10 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var component = helpers.mount(require('./index'), { });
component.incrementBar();
var children = component.getEl('root').children;
expect(children[children.length-1].nodeName).to.equal('SPAN');
};

View File

@ -0,0 +1,5 @@
class {
}
<div.hello>Hello<span.hello-count>${input.count}</span></div>

View File

@ -0,0 +1,15 @@
class {
onCreate() {
this.state = {
count: 0,
renderHello: true
};
}
}
<div.root>
[ROOT]
<span>
<hello key="hello" count=state.count if(state.renderHello)/>
</span>
</div>

View File

@ -0,0 +1,21 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var component = helpers.mount(require('./index'), { });
var rootEl = component.el;
var helloCountEl = rootEl.querySelector('span.hello-count');
var helloComponent = component.getComponent('hello');
component.state.count = 1;
component.update();
expect(component.el).to.equal(rootEl);
expect(rootEl.querySelector('span.hello-count').innerHTML).to.equal('1');
expect(rootEl.querySelector('span.hello-count')).to.equal(helloCountEl);
component.state.renderHello = false;
component.update();
expect(helloComponent.isDestroyed()).to.equal(true);
expect(helloComponent.el == null).to.equal(true);
};

View File

@ -0,0 +1,5 @@
class {
}
<div.hello key="helloRoot">Hello: ${input.count}</div>

Some files were not shown because too many files have changed in this diff Show More