Diffing/Keying fixes (#1094)

* Updates the diffing algorithm to use an HTMLFragment node as an abstraction rather than keeping track of startNode and endNode all throughout the diffing algorithm.

* Uses the HTMLFragment for the <${dynamic}> tag and <include> tags to preserve server-rendered content for which the renderBody is not available in the browser.

* Component ids are based on the resulting parent tree (not the owner tree). This means we cannot rely on the ids in the global lookup, so component key/refs are now also stored on the component instance.

* Static node trees are now only auto assigned a key for the top-level node (instead of all nodes).

* Server comment starting markers now have the component's key serialized so the component can be attached to its owner

* Server comment markers no longer have the id on the closing marker, it is stack based.

* Normalize differences between hydration and client-rendering, so post mount the DOM looks the same regardless of whether a component was server or client rendered.

* fix matching up fragments when hydrating by taking components and normalized text nodes into account

* remove wrapping divs in test, should no longer be needed.  address hydration issue with alternate fragment matching approach.

* add fragment to dom if there's a parentNode even if no startNode

* add test for mismatched hydration

* don't detach components before moving

* use fragments to preserve renderBody content

* use ___finishFragment as the incomplete fragment marker

* ensure fragments get destroyed properly and dom node key sequences don't continue from previous renders

* use parent/owner terminology in more places, component ids are now parent scoped, key/ref components are attached to their owner for both client + server render, server comment boundaries include the owner and the key in addition to the fully scoped component id, autokeyed dom nodes are attached to their parent, hydration now uses a stack: ids in ending comment nodes not needed, hydration checks to see if a component has been initialized and will attach keys directly to the owner if so

* add mutation guards for text/comment nodes, add mutation guard for input value

* make component-pages test better represent streaming hydration, fix html/head/body hydration in a better/more generic way

* add test for async rendered keyrefs

* add test for repeated mult-level transclusion

* Autokeyed elements are now stored on the parent rather than the owner. User assigned key/refs are still stored on the owner component. Because of this, user assigned keys are now prefixed (with @) to differentiate them from autokeys. This also has the benefit that assigning numeric keys can no longer conflict with the autokeys.

* add re-rendering the intermediate container to one of the new tests
This commit is contained in:
Michael Rawlings 2018-08-27 11:46:47 -07:00 committed by GitHub
parent e9b08cb41e
commit df79fcc5f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 1034 additions and 716 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "marko",
"version": "4.12.5",
"version": "4.12.5-2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "marko",
"version": "4.12.5",
"version": "4.12.5-2",
"license": "MIT",
"description": "UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.",
"scripts": {

View File

@ -84,6 +84,7 @@ class HtmlElementVDOM extends Node {
this.dynamicAttributes = def.dynamicAttributes;
this.key = def.key;
this.runtimeFlags = def.runtimeFlags;
this.isAutoKeyed = def.isAutoKeyed;
this.isSVG = false;
this.isTextArea = false;
@ -279,7 +280,10 @@ class HtmlElementVDOM extends Node {
createArgs[INDEX_ATTRS] = attributesArg;
}
if (key) {
if (
key &&
(!this.isAutoKeyed || !this.isStatic || this.createElementId)
) {
createArgs[INDEX_KEY] = key;
if (!this.isStatic) {

View File

@ -39,6 +39,7 @@ module.exports = function(node, codegen, vdomUtil) {
var properties = codegen.generateCode(node.getProperties());
var dynamicAttributes = codegen.generateCode(node.dynamicAttributes);
var key = node.key;
var isAutoKeyed = node.isAutoKeyed;
var runtimeFlags = node.runtimeFlags;
var nextConstId = node.nextConstId;
@ -91,7 +92,8 @@ module.exports = function(node, codegen, vdomUtil) {
isHtmlOnly,
dynamicAttributes,
nextConstId,
runtimeFlags
runtimeFlags,
isAutoKeyed
});
if (bodyOnlyIf) {

View File

@ -140,12 +140,6 @@ function checkInputChanged(existingComponent, oldInput, newInput) {
return false;
}
function getNodes(component) {
var nodes = [];
component.___forEachNode(nodes.push.bind(nodes));
return nodes;
}
var componentProto;
/**
@ -157,8 +151,7 @@ function Component(id) {
EventEmitter.call(this);
this.id = id;
this.___state = null;
this.___startNode = null;
this.___endNode = null;
this.___rootNode = null;
this.___subscriptions = null;
this.___domEventListenerHandles = null;
this.___bubblingDomEvents = null; // Used to keep track of bubbling DOM events for components rendered on the server
@ -230,9 +223,9 @@ Component.prototype = componentProto = {
},
getEl: function(key, index) {
if (key) {
return this.___keyedElements[resolveKeyHelper(key, index)];
return this.___keyedElements["@" + resolveKeyHelper(key, index)];
} else {
return this.___startNode;
return this.___rootNode && this.___rootNode.firstChild;
}
},
getEls: function(key) {
@ -248,29 +241,33 @@ Component.prototype = componentProto = {
return els;
},
getComponent: function(key, index) {
return componentLookup[resolveComponentIdHelper(this, key, index)];
var rootNode = this.___keyedElements[resolveKeyHelper(key, index)];
if (/\[\]$/.test(key)) {
// eslint-disable-next-line no-constant-condition
if ("MARKO_DEBUG") {
complain(
"A repeated key[] was passed to getComponent. Use a non-repeating key if there is only one of these components."
);
}
rootNode = rootNode && rootNode[Object.keys(rootNode)[0]];
}
return rootNode && rootNode.___markoComponent;
},
getComponents: function(key) {
key = key + "[]";
var components = [];
var i = 0;
var component;
while (
(component =
componentLookup[resolveComponentIdHelper(this, key, i)])
) {
components.push(component);
i++;
}
return components;
var lookup = this.___keyedElements[key + "[]"];
return lookup
? Object.keys(lookup).map(function(key) {
return lookup[key].___markoComponent;
})
: [];
},
destroy: function() {
if (this.___destroyed) {
return;
}
var nodes = getNodes(this);
var root = this.___rootNode;
var nodes = this.___rootNode.nodes;
this.___destroyShallow();
@ -282,6 +279,8 @@ Component.prototype = componentProto = {
}
});
root.detached = true;
delete componentLookup[this.id];
},
@ -293,9 +292,9 @@ Component.prototype = componentProto = {
emitLifecycleEvent(this, "destroy");
this.___destroyed = true;
this.___startNode.___markoComponent = undefined;
this.___rootNode.___markoComponent = undefined;
this.___startNode = this.___endNode = null;
this.___rootNode = null;
// Unsubscribe from all DOM events
this.___removeDOMEventListeners();
@ -492,8 +491,7 @@ Component.prototype = componentProto = {
throw TypeError();
}
var startNode = this.___startNode;
var endNodeNextSibling = this.___endNode.nextSibling;
var rootNode = this.___rootNode;
var doc = self.___document;
var input = this.___renderInput || this.___input;
@ -514,16 +512,9 @@ Component.prototype = componentProto = {
var result = new RenderResult(out);
var targetNode = out.___getOutput();
var targetNode = out.___getOutput().___firstChild;
morphdom(
startNode.parentNode,
startNode,
endNodeNextSibling,
targetNode,
doc,
componentsContext
);
morphdom(rootNode, targetNode, doc, componentsContext);
result.afterInsert(doc);
});
@ -532,23 +523,9 @@ Component.prototype = componentProto = {
},
___detach: function() {
var fragment = this.___document.createDocumentFragment();
this.___forEachNode(fragment.appendChild.bind(fragment));
return fragment;
},
___forEachNode: function(callback) {
var currentNode = this.___startNode;
var endNode = this.___endNode;
for (;;) {
var nextSibling = currentNode.nextSibling;
callback(currentNode);
if (currentNode == endNode) {
break;
}
currentNode = nextSibling;
}
var root = this.___rootNode;
root.remove();
return root;
},
___removeDOMEventListeners: function() {
@ -589,12 +566,7 @@ Component.prototype = componentProto = {
'The "this.el" attribute is deprecated. Please use "this.getEl(key)" instead.'
);
}
var el = this.___startNode;
while (el) {
if (el.nodeType === ELEMENT_NODE) return el;
if (el === this.___endNode) return;
el = el.nextSibling;
}
return this.___rootNode && this.___rootNode.firstChild;
},
get els() {
@ -604,7 +576,9 @@ Component.prototype = componentProto = {
'The "this.els" attribute is deprecated. Please use "this.getEls(key)" instead.'
);
}
return getNodes(this).filter(function(el) {
return (this.___rootNode ? this.___rootNode.nodes : []).filter(function(
el
) {
return el.nodeType === ELEMENT_NODE;
});
}

View File

@ -1,6 +1,11 @@
var ComponentDef = require("./ComponentDef");
module.exports = function beginComponent(componentsContext, component) {
module.exports = function beginComponent(
componentsContext,
component,
key,
ownerComponentDef
) {
var componentId = component.id;
var globalContext = componentsContext.___globalContext;
@ -13,6 +18,6 @@ module.exports = function beginComponent(componentsContext, component) {
componentsContext.___components.push(componentDef);
var out = componentsContext.___out;
out.bc(component);
out.bc(component, key, ownerComponentDef && ownerComponentDef.___component);
return componentDef;
};

View File

@ -9,8 +9,9 @@ var FLAG_WILL_RERENDER_IN_BROWSER = 1;
module.exports = function beginComponent(
componentsContext,
component,
key,
ownerComponentDef,
isSplitComponent,
parentComponentDef,
isImplicitComponent
) {
var globalContext = componentsContext.___globalContext;
@ -25,8 +26,8 @@ module.exports = function beginComponent(
// On the server
if (
parentComponentDef &&
parentComponentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER
ownerComponentDef &&
ownerComponentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER
) {
componentDef.___flags |= FLAG_WILL_RERENDER_IN_BROWSER;
return componentDef;
@ -47,9 +48,20 @@ module.exports = function beginComponent(
if (isSplitComponent === false && out.global.noBrowserRerender !== true) {
componentDef.___flags |= FLAG_WILL_RERENDER_IN_BROWSER;
out.w("<!--M#" + componentId + "-->");
}
if (ownerComponentDef && key != null) {
out.w(
"<!--M^" +
componentId +
" " +
ownerComponentDef.id +
" " +
key +
"-->"
);
} else {
out.w("<!--M^" + componentId + "-->");
out.w("<!--M#" + componentId + "-->");
}
return componentDef;

View File

@ -2,6 +2,6 @@
module.exports = function endComponent(out, componentDef) {
if (componentDef.___renderBoundary) {
out.w("<!--M/" + componentDef.id + "-->");
out.w("<!--M/-->");
}
};

View File

@ -3,36 +3,82 @@ var warp10Finalize = require("warp10/finalize");
var eventDelegation = require("./event-delegation");
var win = window;
var defaultDocument = document;
var createFragmentNode = require("../morphdom/fragment").___createFragmentNode;
var componentsUtil = require("./util");
var componentLookup = componentsUtil.___componentLookup;
var addComponentRootToKeyedElements =
componentsUtil.___addComponentRootToKeyedElements;
var ComponentDef = require("./ComponentDef");
var registry = require("./registry");
var serverRenderedGlobals = {};
var serverComponentStartNodes = {};
var serverComponentEndNodes = {};
var serverComponentRootNodes = {};
var keyedElementsByComponentId = {};
var FLAG_WILL_RERENDER_IN_BROWSER = 1;
var FLAG_HAS_BODY_EL = 2;
var FLAG_HAS_HEAD_EL = 4;
function indexServerComponentBoundaries(node) {
function indexServerComponentBoundaries(node, stack) {
var componentId;
var ownerId;
var ownerComponent;
var keyedElements;
var nextSibling;
stack = stack || [];
node = node.firstChild;
while (node) {
nextSibling = node.nextSibling;
if (node.nodeType === 8) {
// Comment node
var commentValue = node.nodeValue;
if (commentValue[0] === "M") {
componentId = commentValue.substring(2);
var firstChar = commentValue[1];
if (firstChar === "/") {
serverComponentEndNodes[componentId] = node;
} else if (firstChar === "^" || firstChar === "#") {
serverComponentStartNodes[componentId] = node;
if (firstChar === "^" || firstChar === "#") {
stack.push(node);
} else if (firstChar === "/") {
var endNode = node;
var startNode = stack.pop();
var rootNode;
if (startNode.parentNode === endNode.parentNode) {
rootNode = createFragmentNode(
startNode.nextSibling,
endNode
);
} else {
rootNode = createFragmentNode(
endNode.parentNode.firstChild,
endNode
);
}
componentId = startNode.nodeValue.substring(2);
firstChar = startNode.nodeValue[1];
if (firstChar === "^") {
var parts = componentId.split(/ /g);
var key = parts[2];
ownerId = parts[1];
componentId = parts[0];
if ((ownerComponent = componentLookup[ownerId])) {
keyedElements = ownerComponent.___keyedElements;
} else {
keyedElements =
keyedElementsByComponentId[ownerId] ||
(keyedElementsByComponentId[ownerId] = {});
}
addComponentRootToKeyedElements(
keyedElements,
key,
rootNode,
componentId
);
}
serverComponentRootNodes[componentId] = rootNode;
startNode.parentNode.removeChild(startNode);
endNode.parentNode.removeChild(endNode);
}
}
} else if (node.nodeType === 1) {
@ -41,11 +87,15 @@ function indexServerComponentBoundaries(node) {
var markoProps = node.getAttribute("data-marko");
if (markoKey) {
var separatorIndex = markoKey.indexOf(" ");
componentId = markoKey.substring(separatorIndex + 1);
ownerId = markoKey.substring(separatorIndex + 1);
markoKey = markoKey.substring(0, separatorIndex);
var keyedElements =
keyedElementsByComponentId[componentId] ||
(keyedElementsByComponentId[componentId] = {});
if ((ownerComponent = componentLookup[ownerId])) {
keyedElements = ownerComponent.___keyedElements;
} else {
keyedElements =
keyedElementsByComponentId[ownerId] ||
(keyedElementsByComponentId[ownerId] = {});
}
keyedElements[markoKey] = node;
}
if (markoProps) {
@ -58,10 +108,10 @@ function indexServerComponentBoundaries(node) {
}
});
}
indexServerComponentBoundaries(node);
indexServerComponentBoundaries(node, stack);
}
node = node.nextSibling;
node = nextSibling;
}
}
@ -238,68 +288,39 @@ function initServerRendered(renderedComponents, doc) {
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];
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 {
delete serverComponentStartNodes[componentId];
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 (!hydrateComponent(componentDef, doc)) {
// hydrateComponent will return false if there is not rootNode
// for the component. If this is the case, we'll wait until the
// DOM has fully loaded to attempt to init the component again.
doc.addEventListener("DOMContentLoaded", function() {
if (!hydrateComponent(componentDef, doc)) {
indexServerComponentBoundaries(doc);
hydrateComponent(componentDef, doc);
}
}
if (endNodeComment) {
delete serverComponentEndNodes[componentId];
endNodeComment.parentNode.removeChild(endNodeComment);
}
});
}
});
}
function hydrateComponent(componentDef, doc) {
var componentId = componentDef.id;
var component = componentDef.___component;
var rootNode = serverComponentRootNodes[componentId];
if (rootNode) {
delete serverComponentRootNodes[componentId];
component.___rootNode = rootNode;
rootNode.___markoComponent = component;
component.___keyedElements =
keyedElementsByComponentId[componentId] || {};
component.___startNode = startNode;
component.___endNode = endNode;
startNode.___markoComponent = component;
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);
});
return true;
}
}
exports.___initClientRendered = initClientRendered;

View File

@ -50,34 +50,24 @@ function createRendererFunc(templateRenderFunc, componentProps) {
var isRerender = component !== undefined;
var id = assignedId;
var isExisting;
var customEvents;
var scope;
var parentComponentDef;
var ownerComponentDef = out.___assignedComponentDef;
var ownerComponentId = ownerComponentDef && ownerComponentDef.id;
var key = out.___assignedKey;
var customEvents = out.___assignedCustomEvents;
if (component) {
id = component.id;
isExisting = true;
globalComponentsContext.___rerenderComponent = null;
} else {
parentComponentDef = componentsContext.___componentDef;
var componentDefFromArgs;
if ((componentDefFromArgs = out.___assignedComponentDef)) {
scope = componentDefFromArgs.id;
if ((parentComponentDef = componentsContext.___componentDef)) {
out.___assignedComponentDef = null;
customEvents = out.___assignedCustomEvents;
var key = out.___assignedKey;
if (key != null) {
key = key.toString();
}
id =
id ||
resolveComponentKey(
globalComponentsContext,
key,
componentDefFromArgs
);
id = id || resolveComponentKey(key, parentComponentDef);
} else if (parentComponentDef) {
id = parentComponentDef.___nextComponentId();
} else {
@ -94,7 +84,7 @@ function createRendererFunc(templateRenderFunc, componentProps) {
out,
typeName,
customEvents,
scope
ownerComponentId
);
} else {
if (!component) {
@ -185,8 +175,9 @@ function createRendererFunc(templateRenderFunc, componentProps) {
var componentDef = beginComponent(
componentsContext,
component,
isSplit,
parentComponentDef
key,
ownerComponentDef,
isSplit
);
// This is a hack, but we have to swap out the component instance stored with this node
@ -227,11 +218,11 @@ function createRendererFunc(templateRenderFunc, componentProps) {
if (customEvents && componentDef.___component) {
if (registry.___isServer) {
componentDef.___customEvents = customEvents;
componentDef.___scope = scope;
componentDef.___scope = ownerComponentId;
} else {
componentDef.___component.___setCustomEvents(
customEvents,
scope
ownerComponentId
);
}
}

View File

@ -12,17 +12,11 @@ var endComponent = require("./endComponent");
var COMPONENT_BEGIN_ASYNC_ADDED_KEY = "$wa";
function resolveComponentKey(
globalComponentsContext,
key,
ownerComponentDef,
parentComponentDef
) {
function resolveComponentKey(key, parentComponentDef) {
if (key[0] === "#") {
return key.substring(1);
} else {
parentComponentDef = parentComponentDef || ownerComponentDef;
return parentComponentDef.___nextKey(ownerComponentDef.id + "-" + key);
return parentComponentDef.id + "-" + parentComponentDef.___nextKey(key);
}
}
@ -77,9 +71,10 @@ function createRendererFunc(
var id;
var isExisting;
var customEvents;
var scope;
var parentComponentDef = componentsContext.___componentDef;
var ownerComponentDef = out.___assignedComponentDef;
var ownerComponentId = ownerComponentDef && ownerComponentDef.id;
var key = out.___assignedKey;
if (component) {
// If component is provided then we are currently rendering
@ -93,23 +88,17 @@ function createRendererFunc(
// DOM (if any) so we will need to resolve the component ID from
// the assigned key. We also need to handle any custom event bindings
// that were provided.
if (ownerComponentDef) {
if (parentComponentDef) {
// console.log('componentArgs:', componentArgs);
scope = ownerComponentDef.id;
out.___assignedComponentDef = null;
customEvents = out.___assignedCustomEvents;
var key = out.___assignedKey;
if (key != null) {
id = resolveComponentKey(
globalComponentsContext,
key.toString(),
ownerComponentDef,
parentComponentDef
);
} else {
id = ownerComponentDef.___nextComponentId();
id = parentComponentDef.___nextComponentId();
}
} else {
id = globalComponentsContext.___nextComponentId();
@ -128,7 +117,7 @@ function createRendererFunc(
out,
typeName,
customEvents,
scope
ownerComponentId
);
// This is the final input after running the lifecycle methods.
@ -176,7 +165,10 @@ function createRendererFunc(
component.___updateQueued = true;
if (customEvents !== undefined) {
component.___setCustomEvents(customEvents, scope);
component.___setCustomEvents(
customEvents,
ownerComponentId
);
}
if (isExisting === false) {
@ -212,8 +204,9 @@ function createRendererFunc(
var componentDef = beginComponent(
componentsContext,
component,
isSplit,
key,
ownerComponentDef,
isSplit,
isImplicitComponent
);

View File

@ -88,7 +88,6 @@ module.exports = function assignComponentId(isRepeated) {
true /* user assigned key */
);
} else {
idExpression = assignedKey;
if (el.data.userAssignedKey !== false) {
if (
context.data.hasLegacyForKey ||
@ -101,7 +100,18 @@ module.exports = function assignComponentId(isRepeated) {
}
}
el.setKey(assignedKey);
// mark as user-assigned key (@)
if (assignedKey.type === "Literal") {
idExpression = builder.literal("@" + assignedKey.value);
} else {
idExpression = builder.binaryExpression(
builder.literal("@"),
"+",
assignedKey
);
}
el.setKey(idExpression);
this.serializeKey();
}
} else {
@ -109,6 +119,8 @@ module.exports = function assignComponentId(isRepeated) {
let parentForKey = getParentForKeyVar(el, this);
let uniqueKey = this.nextUniqueId();
el.isAutoKeyed = true;
nestedIdExpression = isRepeated
? builder.literal(uniqueKey + "[]")
: builder.literal(uniqueKey.toString());

View File

@ -1,10 +1,6 @@
"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) {
let context = this.context;
let builder = this.builder;
@ -87,31 +83,6 @@ module.exports = function handleComponentBind(options) {
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) {

View File

@ -6,6 +6,14 @@ var componentLookup = {};
var defaultDocument = document;
var EMPTY_OBJECT = {};
function getParentComponentForEl(node) {
while (node && !node.___markoComponent) {
node = node.previousSibling || node.parentNode;
node = (node && node.fragment) || node;
}
return node && node.___markoComponent;
}
function getComponentForEl(el, doc) {
if (el) {
var node =
@ -13,7 +21,7 @@ function getComponentForEl(el, doc) {
? (doc || defaultDocument).getElementById(el)
: el;
if (node) {
return node.___markoComponent;
return getParentComponentForEl(node);
}
}
}
@ -50,7 +58,7 @@ function emitLifecycleEvent(component, eventType, eventArg1, eventArg2) {
}
function destroyComponentForNode(node) {
var componentToDestroy = node.___markoComponent;
var componentToDestroy = (node.fragment || node).___markoComponent;
if (componentToDestroy) {
componentToDestroy.___destroyShallow();
delete componentLookup[componentToDestroy.id];
@ -58,17 +66,23 @@ function destroyComponentForNode(node) {
}
function destroyNodeRecursive(node, component) {
destroyComponentForNode(node);
if (node.nodeType === 1) {
if (node.nodeType === 1 || node.nodeType === 12) {
var key;
if (component && (key = node.___markoKey)) {
if (node === component.___keyedElements[key]) {
delete component.___keyedElements[key];
if (node.___markoComponent && /\[\]$/.test(key)) {
delete component.___keyedElements[key][
node.___markoComponent.id
];
} else {
delete component.___keyedElements[key];
}
}
}
var curChild = node.firstChild;
while (curChild) {
while (curChild && curChild !== node.endNode) {
destroyNodeRecursive(curChild, component);
curChild = curChild.nextSibling;
}
@ -122,6 +136,28 @@ function getMarkoPropsFromEl(el) {
return virtualProps;
}
function normalizeComponentKey(key, parentId) {
if (key[0] === "#") {
key = key.replace("#" + parentId + "-", "");
}
return key;
}
function addComponentRootToKeyedElements(
keyedElements,
key,
rootNode,
componentId
) {
if (/\[\]$/.test(key)) {
var repeatedElementsForKey = (keyedElements[key] =
keyedElements[key] || {});
repeatedElementsForKey[componentId] = rootNode;
} else {
keyedElements[key] = rootNode;
}
}
exports.___runtimeId = runtimeId;
exports.___componentLookup = componentLookup;
exports.___getComponentForEl = getComponentForEl;
@ -131,3 +167,5 @@ exports.___destroyNodeRecursive = destroyNodeRecursive;
exports.___nextComponentIdProvider = nextComponentIdProvider;
exports.___attachBubblingEvent = attachBubblingEvent;
exports.___getMarkoPropsFromEl = getMarkoPropsFromEl;
exports.___addComponentRootToKeyedElements = addComponentRootToKeyedElements;
exports.___normalizeComponentKey = normalizeComponentKey;

81
src/morphdom/fragment.js Normal file
View File

@ -0,0 +1,81 @@
var helpers = require("./helpers");
var insertBefore = helpers.___insertBefore;
var fragmentPrototype = {
nodeType: 12,
get firstChild() {
let firstChild = this.startNode.nextSibling;
return firstChild === this.endNode ? undefined : firstChild;
},
get lastChild() {
let lastChild = this.endNode.previousSibling;
return lastChild === this.startNode ? undefined : lastChild;
},
get parentNode() {
let parentNode = this.startNode.parentNode;
return parentNode === this.detachedContainer ? undefined : parentNode;
},
get nextSibling() {
return this.endNode.nextSibling;
},
get nodes() {
const nodes = [];
let current = this.startNode;
while (current !== this.endNode) {
nodes.push(current);
current = current.nextSibling;
}
nodes.push(current);
return nodes;
},
insertBefore(newChildNode, referenceNode) {
const actualReference =
referenceNode == null ? this.endNode : referenceNode;
return insertBefore(
newChildNode,
actualReference,
this.startNode.parentNode
);
},
insertInto(newParentNode, referenceNode) {
this.nodes.forEach(function(node) {
insertBefore(node, referenceNode, newParentNode);
});
return this;
},
remove() {
this.nodes.forEach(function(node) {
this.detachedContainer.appendChild(node);
});
}
};
function createFragmentNode(startNode, nextNode, parentNode) {
var fragment = Object.create(fragmentPrototype);
fragment.startNode = document.createTextNode("");
fragment.endNode = document.createTextNode("");
fragment.startNode.fragment = fragment;
fragment.endNode.fragment = fragment;
var detachedContainer = (fragment.detachedContainer = document.createDocumentFragment());
parentNode =
parentNode || (startNode && startNode.parentNode) || detachedContainer;
insertBefore(fragment.startNode, startNode, parentNode);
insertBefore(fragment.endNode, nextNode, parentNode);
return fragment;
}
function beginFragmentNode(startNode, parentNode) {
var fragment = createFragmentNode(startNode, null, parentNode);
fragment.___finishFragment = function(nextNode) {
fragment.___finishFragment = null;
insertBefore(
fragment.endNode,
nextNode,
parentNode || startNode.parentNode
);
};
return fragment;
}
exports.___createFragmentNode = createFragmentNode;
exports.___beginFragmentNode = beginFragmentNode;

42
src/morphdom/helpers.js Normal file
View File

@ -0,0 +1,42 @@
function insertBefore(node, referenceNode, parentNode) {
if (node.insertInto) {
return node.insertInto(parentNode, referenceNode);
}
return parentNode.insertBefore(
node,
(referenceNode && referenceNode.startNode) || referenceNode
);
}
function insertAfter(node, referenceNode, parentNode) {
return insertBefore(
node,
referenceNode && referenceNode.nextSibling,
parentNode
);
}
function nextSibling(node) {
var next = node.nextSibling;
var fragment = next && next.fragment;
if (fragment) {
return next === fragment.startNode ? fragment : null;
}
return next;
}
function firstChild(node) {
var next = node.firstChild;
return (next && next.fragment) || next;
}
function removeChild(node) {
if (node.remove) node.remove();
else node.parentNode.removeChild(node);
}
exports.___insertBefore = insertBefore;
exports.___insertAfter = insertAfter;
exports.___nextSibling = nextSibling;
exports.___firstChild = firstChild;
exports.___removeChild = removeChild;

View File

@ -3,15 +3,29 @@ var specialElHandlers = require("./specialElHandlers");
var componentsUtil = require("../components/util");
var existingComponentLookup = componentsUtil.___componentLookup;
var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive;
var addComponentRootToKeyedElements =
componentsUtil.___addComponentRootToKeyedElements;
var normalizeComponentKey = componentsUtil.___normalizeComponentKey;
var VElement = require("../runtime/vdom/vdom").___VElement;
var virtualizeElement = VElement.___virtualize;
var morphAttrs = VElement.___morphAttrs;
var eventDelegation = require("../components/event-delegation");
var fragment = require("./fragment");
var helpers = require("./helpers");
var insertBefore = helpers.___insertBefore;
var insertAfter = helpers.___insertAfter;
var nextSibling = helpers.___nextSibling;
var firstChild = helpers.___firstChild;
var removeChild = helpers.___removeChild;
var createFragmentNode = fragment.___createFragmentNode;
var beginFragmentNode = fragment.___beginFragmentNode;
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
var COMPONENT_NODE = 2;
var FRAGMENT_NODE = 12;
// var FLAG_IS_SVG = 1;
// var FLAG_IS_TEXTAREA = 2;
@ -19,6 +33,10 @@ var COMPONENT_NODE = 2;
var FLAG_PRESERVE = 8;
// var FLAG_CUSTOM_ELEMENT = 16;
function isAutoKey(key) {
return !/^@/.test(key);
}
function compareNodeNames(fromEl, toEl) {
return fromEl.___nodeName === toEl.___nodeName;
}
@ -29,54 +47,40 @@ function onNodeAdded(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(
parentNode,
startNode,
endNode,
toNode,
doc,
componentsContext
) {
function morphdom(fromNode, toNode, doc, componentsContext) {
var globalComponentsContext;
var isRerenderInBrowser = false;
var keySequences = {};
if (componentsContext) {
globalComponentsContext = componentsContext.___globalContext;
isRerenderInBrowser = globalComponentsContext.___isRerenderInBrowser;
}
function createMarkerComment() {
return doc.createComment("$marko");
}
function insertVirtualNodeBefore(
vNode,
key,
referenceEl,
parentEl,
component
ownerComponent,
parentComponent
) {
var realNode = vNode.___actualize(doc);
insertBefore(realNode, referenceEl, parentEl);
if (vNode.___nodeType === ELEMENT_NODE) {
if (
vNode.___nodeType === ELEMENT_NODE ||
vNode.___nodeType === FRAGMENT_NODE
) {
if (key) {
realNode.___markoKey = key;
component.___keyedElements[key] = realNode;
(isAutoKey(key)
? parentComponent
: ownerComponent
).___keyedElements[key] = realNode;
}
morphChildren(realNode, null, null, vNode, component);
morphChildren(realNode, vNode, parentComponent);
}
onNodeAdded(realNode, componentsContext);
@ -86,144 +90,45 @@ function morphdom(
vComponent,
referenceNode,
referenceNodeParentEl,
component
component,
key,
ownerComponent,
parentComponent
) {
component.___startNode = component.___endNode = insertBefore(
createMarkerComment(),
var rootNode = (component.___rootNode = insertBefore(
createFragmentNode(),
referenceNode,
referenceNodeParentEl
);
morphComponent(referenceNodeParentEl, component, vComponent);
));
rootNode.___markoComponent = component;
if (key && ownerComponent) {
key = normalizeComponentKey(key, parentComponent.id);
addComponentRootToKeyedElements(
ownerComponent.___keyedElements,
key,
rootNode,
component.id
);
rootNode.___markoKey = key;
}
morphComponent(component, vComponent);
}
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;
}
if (endNode === startNode) {
return insertAfter(createMarkerComment(), startNode, parentNode);
}
return endNode;
}
function morphComponent(parentFromNode, component, vComponent) {
// We create a key sequence to generate unique keys since a key
// can be repeated
component.___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;
var tempChild;
if (!beforeChild) {
tempChild = beforeChild = insertBefore(
createMarkerComment(),
startNode,
parentFromNode
);
}
morphChildren(
parentFromNode,
startNode,
afterChild,
vComponent,
component
);
endNode = undefined;
startNode = beforeChild.nextSibling;
if (!startNode || startNode === afterChild) {
startNode = endNode = insertAfter(
createMarkerComment(),
beforeChild,
parentFromNode
);
}
if (tempChild) {
parentFromNode.removeChild(tempChild);
}
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;
component.___keySequence = undefined; // We don't need to track keys anymore
return afterChild;
function morphComponent(component, vComponent) {
morphChildren(component.___rootNode, vComponent, component);
}
var detachedNodes = [];
function detachNode(node, parentNode, component) {
if (node.nodeType === ELEMENT_NODE) {
function detachNode(node, parentNode, ownerComponent) {
if (node.nodeType === ELEMENT_NODE || node.nodeType === FRAGMENT_NODE) {
detachedNodes.push(node);
node.___markoDetached = component || true;
node.___markoDetached = ownerComponent || true;
} else {
destroyNodeRecursive(node);
parentNode.removeChild(node);
removeChild(node);
}
}
@ -231,14 +136,8 @@ function morphdom(
component.destroy();
}
function morphChildren(
parentFromNode,
startNode,
endNode,
toNode,
component
) {
var curFromNodeChild = startNode;
function morphChildren(fromNode, toNode, parentComponent) {
var curFromNodeChild = firstChild(fromNode);
var curToNodeChild = toNode.___firstChild;
var curToNodeKey;
@ -255,56 +154,57 @@ function morphdom(
outer: while (curToNodeChild) {
toNextSibling = curToNodeChild.___nextSibling;
curToNodeType = curToNodeChild.___nodeType;
curToNodeKey = curToNodeChild.___key;
var componentForNode = curToNodeChild.___component || component;
var ownerComponent =
curToNodeChild.___ownerComponent || parentComponent;
var referenceComponent;
if (curToNodeType === COMPONENT_NODE) {
var component = curToNodeChild.___component;
if (
(matchingFromComponent =
existingComponentLookup[componentForNode.id]) ===
undefined
existingComponentLookup[component.id]) === undefined
) {
if (isRerenderInBrowser === true) {
var firstVChild = curToNodeChild.___firstChild;
if (firstVChild) {
if (!curFromNodeChild) {
curFromNodeChild = insertBefore(
createMarkerComment(),
null,
parentFromNode
);
}
var rootNode = beginFragmentNode(
curFromNodeChild,
fromNode
);
component.___rootNode = rootNode;
rootNode.___markoComponent = component;
componentForNode.___startNode = curFromNodeChild;
componentForNode.___endNode = resolveComponentEndNode(
curFromNodeChild,
firstVChild,
parentFromNode
if (ownerComponent && curToNodeKey) {
curToNodeKey = normalizeComponentKey(
curToNodeKey,
parentComponent.id
);
} else {
componentForNode.___startNode = componentForNode.___endNode = insertBefore(
createMarkerComment(),
curFromNodeChild,
parentFromNode
addComponentRootToKeyedElements(
ownerComponent.___keyedElements,
curToNodeKey,
rootNode,
component.id
);
rootNode.___markoKey = curToNodeKey;
}
curFromNodeChild = morphComponent(
parentFromNode,
componentForNode,
curToNodeChild
);
morphComponent(component, curToNodeChild);
curFromNodeChild = nextSibling(rootNode);
} else {
insertVirtualComponentBefore(
curToNodeChild,
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
component,
curToNodeKey,
ownerComponent,
parentComponent
);
}
} else {
if (
matchingFromComponent.___startNode !== curFromNodeChild
matchingFromComponent.___rootNode !== curFromNodeChild
) {
if (
curFromNodeChild &&
@ -316,66 +216,67 @@ function morphdom(
) {
// 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;
curFromNodeChild = nextSibling(
fromComponent.___rootNode
);
destroyComponent(fromComponent);
continue;
}
// We need to move the existing component into
// the correct location and preserve focus.
var activeElement = doc.activeElement;
// the correct location
insertBefore(
matchingFromComponent.___detach(),
matchingFromComponent.___rootNode,
curFromNodeChild,
parentFromNode
fromNode
);
// This focus patch should be a temporary fix.
if (
activeElement !== doc.activeElement &&
activeElement.focus
) {
activeElement.focus();
}
} else {
curFromNodeChild =
curFromNodeChild && nextSibling(curFromNodeChild);
}
if (curToNodeChild.___preserve) {
curFromNodeChild =
matchingFromComponent.___endNode.nextSibling;
} else {
curFromNodeChild = morphComponent(
parentFromNode,
componentForNode,
curToNodeChild
);
if (!curToNodeChild.___preserve) {
morphComponent(component, curToNodeChild);
}
}
curToNodeChild = toNextSibling;
continue;
} else if ((curToNodeKey = curToNodeChild.___key)) {
} else if (curToNodeKey) {
curVFromNodeChild = undefined;
curFromNodeKey = undefined;
if (isAutoKey(curToNodeKey)) {
if (ownerComponent !== parentComponent) {
curToNodeKey += ":" + ownerComponent.id;
}
referenceComponent = parentComponent;
} else {
referenceComponent = ownerComponent;
}
var keySequence =
componentForNode.___keySequence ||
(componentForNode.___keySequence = globalComponentsContext.___createKeySequence());
keySequences[referenceComponent.id] ||
(keySequences[
referenceComponent.id
] = globalComponentsContext.___createKeySequence());
// We have a keyed element. This is the fast path for matching
// up elements
curToNodeKey = keySequence.___nextKey(curToNodeKey);
if (curFromNodeChild) {
if (curFromNodeChild !== endNode) {
curFromNodeKey = curFromNodeChild.___markoKey;
curVFromNodeChild = curFromNodeChild.___markoVElement;
fromNextSibling = curFromNodeChild.nextSibling;
}
curFromNodeKey = curFromNodeChild.___markoKey;
curVFromNodeChild = curFromNodeChild.___markoVElement;
fromNextSibling = nextSibling(curFromNodeChild);
}
if (curFromNodeKey === curToNodeKey) {
// Elements line up. Now we just have to make sure they are compatible
if ((curToNodeChild.___flags & FLAG_PRESERVE) === 0) {
if (
(curToNodeChild.___flags & FLAG_PRESERVE) === 0 &&
!curToNodeChild.___preserve
) {
// We just skip over the fromNode if it is preserved
if (
@ -385,15 +286,16 @@ function morphdom(
curFromNodeChild,
curVFromNodeChild,
curToNodeChild,
componentForNode,
curToNodeKey
curToNodeKey,
ownerComponent,
parentComponent
);
} else {
// Remove the old node
detachNode(
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent
);
// Incompatible nodes. Just move the target VNode into the DOM at this position
@ -401,8 +303,9 @@ function morphdom(
curToNodeChild,
curToNodeKey,
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent,
parentComponent
);
}
} else {
@ -411,38 +314,76 @@ function morphdom(
} else {
if (
(matchingFromEl =
componentForNode.___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,
componentForNode,
referenceComponent.___keyedElements[
curToNodeKey
);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue;
]) === undefined
) {
if (isRerenderInBrowser === true && curFromNodeChild) {
if (
curFromNodeChild.nodeType === ELEMENT_NODE &&
curFromNodeChild.nodeName ===
curToNodeChild.___nodeName
) {
curVFromNodeChild = virtualizeElement(
curFromNodeChild
);
curFromNodeChild.___markoKey = curToNodeKey;
morphEl(
curFromNodeChild,
curVFromNodeChild,
curToNodeChild,
curToNodeKey,
ownerComponent,
parentComponent
);
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue;
} else if (
curToNodeChild.___nodeType === FRAGMENT_NODE &&
curFromNodeChild.nodeType === COMMENT_NODE
) {
var content = curFromNodeChild.nodeValue;
if (content == "F#" + curToNodeKey) {
var endNode = curFromNodeChild;
while (
endNode.nodeType !== COMMENT_NODE ||
endNode.nodeValue !== "F/"
)
endNode = endNode.nextSibling;
var fragment = createFragmentNode(
curFromNodeChild,
endNode.nextSibling,
fromNode
);
fragment.___markoKey = curToNodeKey;
fragment.___markoVElement = curToNodeChild;
removeChild(curFromNodeChild);
removeChild(endNode);
if (!curToNodeChild.___preserve) {
morphChildren(
fragment,
curToNodeChild,
parentComponent
);
}
curToNodeChild = toNextSibling;
curFromNodeChild = fragment.nextSibling;
continue;
}
}
}
insertVirtualNodeBefore(
curToNodeChild,
curToNodeKey,
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent,
parentComponent
);
fromNextSibling = curFromNodeChild;
} else {
@ -479,7 +420,7 @@ function morphdom(
insertBefore(
matchingFromEl,
curFromNodeChild,
parentFromNode
fromNode
);
} else {
// Single element removal
@ -488,14 +429,15 @@ function morphdom(
// 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;
fromNextSibling = nextSibling(
fromNextSibling
);
if (curFromNodeChild) {
detachNode(
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent
);
}
}
@ -509,14 +451,14 @@ function morphdom(
insertAfter(
matchingFromEl,
curFromNodeChild,
parentFromNode
fromNode
);
if (curFromNodeChild) {
detachNode(
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent
);
}
}
@ -529,9 +471,9 @@ function morphdom(
matchingFromEl,
curVFromNodeChild,
curToNodeChild,
componentForNode,
curToNodeKey,
curToNodeKey
ownerComponent,
parentComponent
);
}
} else {
@ -539,13 +481,14 @@ function morphdom(
curToNodeChild,
curToNodeKey,
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent,
parentComponent
);
detachNode(
matchingFromEl,
parentFromNode,
componentForNode
fromNode,
ownerComponent
);
}
}
@ -559,18 +502,17 @@ function morphdom(
// 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 !== componentForNode
) {
while (curFromNodeChild) {
fromNextSibling = nextSibling(curFromNodeChild);
if ((fromComponent = curFromNodeChild.___markoComponent)) {
// 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;
curFromNodeChild = fromNextSibling;
if (
!globalComponentsContext.___renderedComponentsById[
@ -583,8 +525,6 @@ function morphdom(
continue; // Move to the next "from" node
}
fromNextSibling = curFromNodeChild.nextSibling;
var curFromNodeType = curFromNodeChild.nodeType;
var isCompatible = undefined;
@ -626,8 +566,9 @@ function morphdom(
curFromNodeChild,
curVFromNodeChild,
curToNodeChild,
component,
curToNodeKey
curToNodeKey,
ownerComponent,
parentComponent
);
}
} else if (
@ -638,44 +579,10 @@ function morphdom(
isCompatible = true;
// Simply update nodeValue on the original node to
// change the text value
var content = curFromNodeChild.nodeValue;
if (content == curToNodeChild.___nodeValue) {
if (/^F\^/.test(content)) {
var closingContent = content.replace(
/^F\^/,
"F/"
);
while (
(curFromNodeChild =
curFromNodeChild.nextSibling)
) {
if (
curFromNodeChild.nodeValue ===
closingContent
) {
break;
}
}
while (
(curToNodeChild =
curToNodeChild.___nextSibling)
) {
if (
curToNodeChild.___nodeValue ===
closingContent
) {
break;
}
}
curToNodeChild = curToNodeChild.___nextSibling;
curFromNodeChild =
curFromNodeChild === endNode
? null
: curFromNodeChild.nextSibling;
continue outer;
}
} else {
if (
curFromNodeChild.nodeValue !==
curToNodeChild.___nodeValue
) {
curFromNodeChild.nodeValue =
curToNodeChild.___nodeValue;
}
@ -695,18 +602,10 @@ function morphdom(
curFromNodeKey
] === undefined
) {
detachNode(
curFromNodeChild,
parentFromNode,
componentForNode
);
detachNode(curFromNodeChild, fromNode, ownerComponent);
}
} else {
detachNode(
curFromNodeChild,
parentFromNode,
componentForNode
);
detachNode(curFromNodeChild, fromNode, ownerComponent);
}
curFromNodeChild = fromNextSibling;
@ -720,54 +619,69 @@ function morphdom(
curToNodeChild,
curToNodeKey,
curFromNodeChild,
parentFromNode,
componentForNode
fromNode,
ownerComponent,
parentComponent
);
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;
// We have processed all of the "to nodes".
if (fromNode.___finishFragment) {
// If we are in an unfinished fragment, we have reached the end of the nodes
// we were matching up and need to end the fragment
fromNode.___finishFragment(curFromNodeChild);
} else {
// If curFromNodeChild is non-null then we still have some from nodes
// left over that need to be removed
while (curFromNodeChild) {
fromNextSibling = nextSibling(curFromNodeChild);
if ((fromComponent = curFromNodeChild.___markoComponent)) {
curFromNodeChild = fromComponent.___endNode.nextSibling;
if (
!globalComponentsContext.___renderedComponentsById[
fromComponent.id
]
) {
destroyComponent(fromComponent);
if ((fromComponent = curFromNodeChild.___markoComponent)) {
curFromNodeChild = fromNextSibling;
if (
!globalComponentsContext.___renderedComponentsById[
fromComponent.id
]
) {
destroyComponent(fromComponent);
}
continue;
}
continue;
curVFromNodeChild = curFromNodeChild.___markoVElement;
// For transcluded content, we need to check if the element belongs to a different component
// context than the current component and ensure it gets removed from its key index.
if (isAutoKey(fromNode.___markoKey)) {
referenceComponent = parentComponent;
} else {
referenceComponent =
curVFromNodeChild &&
curVFromNodeChild.___ownerComponent;
}
detachNode(curFromNodeChild, fromNode, referenceComponent);
curFromNodeChild = fromNextSibling;
}
curVFromNodeChild = curFromNodeChild.___markoVElement;
// For transcluded content, we need to check if the element belongs to a different component
// context than the current component and ensure it gets removed from its key index.
fromComponent =
(curVFromNodeChild && curVFromNodeChild.___component) ||
component;
detachNode(curFromNodeChild, parentFromNode, fromComponent);
curFromNodeChild = fromNextSibling;
}
}
function morphEl(fromEl, vFromEl, toEl, component, toElKey) {
function morphEl(
fromEl,
vFromEl,
toEl,
toElKey,
ownerComponent,
parentComponent
) {
var nodeName = toEl.___nodeName;
if (isRerenderInBrowser === true && toElKey) {
component.___keyedElements[toElKey] = fromEl;
ownerComponent.___keyedElements[toElKey] = fromEl;
}
var constId = toEl.___constId;
@ -786,7 +700,7 @@ function morphdom(
}
if (nodeName !== "TEXTAREA") {
morphChildren(fromEl, fromEl.firstChild, null, toEl, component);
morphChildren(fromEl, toEl, parentComponent);
}
var specialElHandler = specialElHandlers[nodeName];
@ -795,7 +709,7 @@ function morphdom(
}
} // END: morphEl(...)
morphChildren(parentNode, startNode, endNode, toNode);
morphChildren(fromNode, toNode, toNode.___component);
detachedNodes.forEach(function(node) {
var detachedFromComponent = node.___markoDetached;
@ -813,7 +727,7 @@ function morphdom(
);
if (eventDelegation.___handleNodeDetach(node) != false) {
node.parentNode.removeChild(node);
removeChild(node);
}
}
}

View File

@ -33,7 +33,7 @@ SpecialElHandlers.prototype = {
fromEl.value = toEl.___value;
}
if (!toEl.___hasAttribute("value")) {
if (fromEl.hasAttribute("value") && !toEl.___hasAttribute("value")) {
fromEl.removeAttribute("value");
}
},
@ -63,18 +63,21 @@ SpecialElHandlers.prototype = {
SELECT: function(fromEl, toEl) {
if (!toEl.___hasAttribute("multiple")) {
var i = -1;
var selected = 0;
var curChild = toEl.___firstChild;
while (curChild) {
if (curChild.___nodeName == "OPTION") {
i++;
if (curChild.___hasAttribute("selected")) {
break;
selected = i;
}
}
curChild = curChild.___nextSibling;
}
fromEl.selectedIndex = i;
if (fromEl.selectedIndex !== selected) {
fromEl.selectedIndex = selected;
}
}
}
};

View File

@ -1,5 +1,8 @@
"use strict";
var removeDashes = require("../compiler/util/removeDashes");
var ComponentsContext = require("../components/ComponentsContext");
var getComponentsContext = ComponentsContext.___getComponentsContext;
var ComponentDef = require("../components/ComponentDef");
var isArray = Array.isArray;
var RENDER_BODY_TOKEN = "%FN";
var RENDER_BODY_TO_JSON = function() {
@ -105,8 +108,8 @@ var helpers = {
*/
d: function dynamicTag(tag, attrs, out, componentDef, key, customEvents) {
if (tag) {
var component = componentDef && componentDef.___component;
if (typeof tag === "string") {
var component = componentDef && componentDef.component;
var events =
customEvents &&
customEvents.reduce(function(events, eventArray) {
@ -161,17 +164,28 @@ var helpers = {
if (isFn || isToken) {
var flags = componentDef ? componentDef.___flags : 0;
var parentId = componentDef ? componentDef.id : "";
var willRerender =
flags & FLAG_WILL_RERENDER_IN_BROWSER;
var resolvedKey = parentId + " " + key;
var insertMarkers = IS_SERVER ? willRerender : isToken;
if (insertMarkers) out.comment("F^" + resolvedKey);
var preserve = IS_SERVER ? willRerender : isToken;
out.___beginFragment(key, component, preserve);
if (isFn) {
var componentsContext = getComponentsContext(out);
var parentComponentDef =
componentsContext.___componentDef;
var globalContext =
componentsContext.___globalContext;
componentsContext.___componentDef = new ComponentDef(
component,
parentComponentDef.id +
"-" +
parentComponentDef.___nextKey(key),
globalContext
);
render.toJSON = RENDER_BODY_TO_JSON;
render(out, attrs);
componentsContext.___componentDef = parentComponentDef;
}
if (insertMarkers) out.comment("F/" + resolvedKey);
out.___endFragment();
} else {
out.error("Invalid dynamic tag value");
}

View File

@ -500,6 +500,24 @@ var proto = (AsyncStream.prototype = {
this.write(escapeXml(str));
},
___beginFragment: function(key, component, preserve) {
if (preserve) {
this.write("<!--F#" + escapeXml(key) + "-->");
}
if (this._elStack) {
this._elStack.push(preserve);
} else {
this._elStack = [preserve];
}
},
___endFragment: function() {
var preserve = this._elStack.pop();
if (preserve) {
this.write("<!--F/-->");
}
},
___getNode: function(doc) {
var node = this._node;
var curEl;

View File

@ -5,6 +5,7 @@ var VDocumentFragment = vdom.___VDocumentFragment;
var VComment = vdom.___VComment;
var VText = vdom.___VText;
var VComponent = vdom.___VComponent;
var VFragment = vdom.___VFragment;
var virtualizeHTML = vdom.___virtualizeHTML;
var RenderResult = require("../RenderResult");
var defaultDocument = vdom.___defaultDocument;
@ -56,13 +57,13 @@ var proto = (AsyncVDOMBuilder.prototype = {
___isOut: true,
___document: defaultDocument,
bc: function(component) {
var vComponent = new VComponent(component);
bc: function(component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent);
return this.___beginNode(vComponent, 0, true);
},
___preserveComponent: function(component) {
var vComponent = new VComponent(component, true);
___preserveComponent: function(component, key, ownerComponent) {
var vComponent = new VComponent(component, key, ownerComponent, true);
this.___beginNode(vComponent, 0);
},
@ -122,7 +123,7 @@ var proto = (AsyncVDOMBuilder.prototype = {
// and a node can only have one parent node.
var clone = node.___cloneNode();
this.node(clone);
clone.___component = component;
clone.___ownerComponent = component;
return this;
},
@ -208,6 +209,16 @@ var proto = (AsyncVDOMBuilder.prototype = {
return this;
},
___beginFragment: function(key, component, preserve) {
var fragment = new VFragment(key, component, preserve);
this.___beginNode(fragment, null, true);
return this;
},
___endFragment: function() {
this.endElement();
},
endElement: function() {
var stack = this.___stack;
stack.pop();
@ -417,7 +428,7 @@ var proto = (AsyncVDOMBuilder.prototype = {
// 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);
morphdom(node, vdomTree, doc, this.___components);
}
return node;
},

View File

@ -1,9 +1,11 @@
var VNode = require("./VNode");
var inherit = require("raptor-util/inherit");
function VComponent(component, preserve) {
function VComponent(component, key, ownerComponent, preserve) {
this.___VNode(null /* childCount */);
this.___key = key;
this.___component = component;
this.___ownerComponent = ownerComponent;
this.___preserve = preserve;
}

View File

@ -60,7 +60,15 @@ function VElementClone(other) {
this.___isTextArea = other.___isTextArea;
}
function VElement(tagName, attrs, key, component, childCount, flags, props) {
function VElement(
tagName,
attrs,
key,
ownerComponent,
childCount,
flags,
props
) {
this.___VNode(childCount);
var constId;
@ -81,7 +89,7 @@ function VElement(tagName, attrs, key, component, childCount, flags, props) {
}
this.___key = key;
this.___component = component;
this.___ownerComponent = ownerComponent;
this.___attributes = attrs || EMPTY_OBJECT;
this.___properties = props || EMPTY_OBJECT;
this.___namespaceURI = namespaceURI;
@ -105,13 +113,13 @@ 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, key, component, childCount, flags, props) {
e: function(tagName, attrs, key, ownerComponent, childCount, flags, props) {
var child = this.___appendChild(
new VElement(
tagName,
attrs,
key,
component,
ownerComponent,
childCount,
flags,
props
@ -132,13 +140,21 @@ 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, key, component, childCount, flags, props) {
ed: function(
tagName,
attrs,
key,
ownerComponent,
childCount,
flags,
props
) {
var child = this.___appendChild(
VElement.___createElementDynamicTag(
tagName,
attrs,
key,
component,
ownerComponent,
childCount,
flags,
props
@ -158,9 +174,9 @@ VElement.prototype = {
*
* @param {String} value The value for the new Comment node
*/
n: function(node, component) {
n: function(node, ownerComponent) {
node = node.___cloneNode();
node.___component = component;
node.___ownerComponent = ownerComponent;
this.___appendChild(node);
return this.___finishChild();
},
@ -238,7 +254,12 @@ defineProperty(proto, "___value", {
if (value == null) {
value = this.___attributes.value;
}
return value != null ? toString(value) : "";
return value != null
? toString(value)
: this.___attributes.type === "checkbox" ||
this.___attributes.type === "radio"
? "on"
: "";
}
});
@ -246,7 +267,7 @@ VElement.___createElementDynamicTag = function(
tagName,
attrs,
key,
component,
ownerComponent,
childCount,
flags,
props
@ -257,7 +278,7 @@ VElement.___createElementDynamicTag = function(
tagName,
attrs,
key,
component,
ownerComponent,
childCount,
flags,
props
@ -307,7 +328,7 @@ function virtualizeElement(node, virtualizeChildNodes) {
tagName,
attrs,
null /*key*/,
null /*component*/,
null /*ownerComponent*/,
0 /*child count*/,
flags,
null /*props*/

View File

@ -0,0 +1,25 @@
var VNode = require("./VNode");
var inherit = require("raptor-util/inherit");
var createFragmentNode = require("../../morphdom/fragment")
.___createFragmentNode;
function VFragment(key, ownerComponent, preserve) {
this.___VNode(null /* childCount */);
this.___key = key;
this.___ownerComponent = ownerComponent;
this.___preserve = preserve;
}
VFragment.prototype = {
___nodeType: 12,
___actualize: function() {
var fragment = createFragmentNode();
fragment.___markoKey = this.___key;
fragment.___markoVElement = this;
return fragment;
}
};
inherit(VFragment, VNode);
module.exports = VFragment;

View File

@ -11,7 +11,7 @@ VNode.prototype = {
this.___nextSiblingInternal = null;
},
___component: null,
___ownerComponent: null,
get ___firstChild() {
var firstChild = this.___firstChildInternal;

View File

@ -4,6 +4,7 @@ var VDocumentFragment = require("./VDocumentFragment");
var VElement = require("./VElement");
var VText = require("./VText");
var VComponent = require("./VComponent");
var VFragment = require("./VFragment");
var defaultDocument = typeof document != "undefined" && document;
var specialHtmlRegexp = /[&<]/;
@ -91,6 +92,7 @@ exports.___VDocumentFragment = VDocumentFragment;
exports.___VElement = VElement;
exports.___VText = VText;
exports.___VComponent = VComponent;
exports.___VFragment = VFragment;
exports.___virtualize = virtualize;
exports.___virtualizeHTML = virtualizeHTML;
exports.___defaultDocument = defaultDocument;

View File

@ -37,15 +37,18 @@ function doInclude(input, out, throwError) {
var willRerenderInBrowser =
componentDef &&
componentDef.___flags & FLAG_WILL_RERENDER_IN_BROWSER;
var key = (componentDef && componentDef.id) + " " + out.___assignedKey;
var isFn = renderBody !== RENDER_BODY_TOKEN;
var insertMarkers = (IS_SERVER && willRerenderInBrowser) || !isFn;
if (insertMarkers) out.comment("F^" + key);
if (renderBody !== RENDER_BODY_TOKEN) {
var preserve = (IS_SERVER && willRerenderInBrowser) || !isFn;
out.___beginFragment(
out.___assignedKey,
componentDef && componentDef.___component,
preserve
);
if (isFn) {
renderBody.toJSON = RENDER_BODY_TO_JSON;
renderBody(out, arg);
}
if (insertMarkers) out.comment("F/" + key);
out.___endFragment();
}
}

View File

@ -21,7 +21,7 @@ var marko_template = module.exports = require("marko/src/vdom").t(),
.e("a", {
"xlink:href": "https://developer.mozilla.org/en-US/docs/SVG",
target: "_blank"
}, "1", null, 0, 1);
}, null, null, 0, 1);
function render(input, out, __component, component, state) {
var data = input;

View File

@ -22,7 +22,7 @@ var marko_template = module.exports = require("marko/src/vdom").t(),
cx: "100",
cy: "100",
r: "100"
}, "1", null, 0, 1);
}, null, null, 0, 1);
function render(input, out, __component, component, state) {
var data = input;

View File

@ -0,0 +1,7 @@
class {
onCreate() {
this.message = "Hello";
}
}
<div/>

View File

@ -0,0 +1,3 @@
class {}
<hello key="something[]"/>

View File

@ -0,0 +1,6 @@
var expect = require("chai").expect;
module.exports = function(helpers) {
var component = helpers.mount(require.resolve("./index"), {});
expect(component.getComponent("something[]").message).to.equal("Hello");
};

View File

@ -11,8 +11,8 @@ module.exports = function(helpers) {
var nextSibling = document.createElement("div");
helpers.targetEl.appendChild(nextSibling);
expect(widget.el.previousSibling).to.equal(previousSibling);
expect(widget.el.nextSibling).to.equal(nextSibling);
expect(widget.el.previousElementSibling).to.equal(previousSibling);
expect(widget.el.nextElementSibling).to.equal(nextSibling);
var parentNode = widget.el.parentNode;
@ -29,10 +29,10 @@ module.exports = function(helpers) {
expect(widget.el.parentNode).to.equal(parentNode);
// expect(widget.el !== oldEl).to.equal(true);
expect(helpers.targetEl.childNodes.length).to.equal(3);
expect(helpers.targetEl.childNodes[0]).to.equal(previousSibling);
expect(helpers.targetEl.childNodes[1]).to.equal(widget.el);
expect(helpers.targetEl.childNodes[2]).to.equal(nextSibling);
expect(helpers.targetEl.children.length).to.equal(3);
expect(helpers.targetEl.children[0]).to.equal(previousSibling);
expect(helpers.targetEl.children[1]).to.equal(widget.el);
expect(helpers.targetEl.children[2]).to.equal(nextSibling);
};
module.exports.skip_hydrate = "a split widget cannot re-render when hydrated";

View File

@ -4,18 +4,12 @@ module.exports = function(helpers) {
var component = helpers.mount(require.resolve("./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(component.getComponent("inner")).to.equal(innerComponent);
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

@ -6,11 +6,18 @@ module.exports = function(helpers) {
color: colors[0]
});
expect(component.events.length).to.equal(1);
expect(component.events[0].color).to.equal("blue");
expect(component.events[0].node).to.equal(
component.el.querySelectorAll("li")[0]
);
if (helpers.isHydrate) {
// When hydrating, the first color item was rendered on the
// server so there is no corresponding attach event f
// we'll push an empty event so the indexes line up
component.events.push(null);
} else {
expect(component.events.length).to.equal(1);
expect(component.events[0].color).to.equal("blue");
expect(component.events[0].node).to.equal(
component.el.querySelectorAll("li")[0]
);
}
component.input = { color: colors[1] };
component.update();

View File

@ -34,5 +34,3 @@ module.exports = function(helpers) {
component.update();
});
};
module.exports.fails = "issue #1059";

View File

@ -1,7 +1,7 @@
class {}
<card for(i in [1,2,3,4])>
<div.body key="transcluded">
<div.body key=`transcluded:${i}`>
<span>${i}</span>
</div>
</card>

View File

@ -5,6 +5,6 @@ module.exports = function(helpers) {
var targetEl = helpers.targetEl;
var innerHTML = targetEl.innerHTML;
expect(innerHTML).to.equal(
'<!--$marko--><div class="card"><div class="body"><span>1</span></div></div><div class="card"><div class="body"><span>2</span></div></div><div class="card"><div class="body"><span>3</span></div></div><div class="card"><div class="body"><span>4</span></div></div><!--$marko-->'
'<div class="card"><div class="body"><span>1</span></div></div><div class="card"><div class="body"><span>2</span></div></div><div class="card"><div class="body"><span>3</span></div></div><div class="card"><div class="body"><span>4</span></div></div>'
);
};

View File

@ -3,5 +3,3 @@ module.exports = function(helpers) {
component.setState("showLast", false);
component.update();
};
module.exports.fails_hydrate = "issue #1051";

View File

@ -9,8 +9,10 @@ module.exports = function(helpers, done) {
.render({ name: "John" })
.then(function(result) {
result.replace(targetEl);
expect(component.el.firstChild.className).to.equal("hello");
expect(component.el.firstChild.innerHTML).to.equal("Hello John");
expect(component.el.firstElementChild.className).to.equal("hello");
expect(component.el.firstElementChild.innerHTML).to.equal(
"Hello John"
);
done();
})
.catch(function(err) {

View File

@ -7,6 +7,6 @@ module.exports = function(helpers) {
var targetEl = component.getEl("target");
hello.renderSync({ name: "John" }).replace(targetEl);
expect(component.el.firstChild.className).to.equal("hello");
expect(component.el.firstChild.innerHTML).to.equal("Hello John");
expect(component.el.firstElementChild.className).to.equal("hello");
expect(component.el.firstElementChild.innerHTML).to.equal("Hello John");
};

View File

@ -0,0 +1,12 @@
class {
onMount() {
}
}
<div.bar>
Hello
</div>
<div.bar>
World
</div>

View File

@ -0,0 +1,8 @@
class {
onMount() {
}
}
<div.foo>
</div>

View File

@ -0,0 +1,10 @@
class {
onMount() {
}
}
<if (typeof window !== 'undefined')>
<app-foo/>
<a href="ebay.com">eBay</a>
</if>

View File

@ -0,0 +1,16 @@
var expect = require("chai").expect;
module.exports = function(helpers) {
var component = helpers.mount(require.resolve("./index"), {});
expect(helpers.targetEl.innerHTML).to.equal(
'<div class="foo"></div><a href="ebay.com">eBay</a>'
);
component.forceUpdate();
component.update();
expect(helpers.targetEl.innerHTML).to.equal(
'<div class="foo"></div><a href="ebay.com">eBay</a>'
);
};

View File

@ -14,5 +14,3 @@ module.exports = function(helpers) {
component.update();
expect(helpers.targetEl.textContent).to.equal("Hello world");
};
module.exports.fails = "issue #1033";

View File

@ -8,24 +8,26 @@ module.exports = function(helpers) {
expect(buttonComponent).to.exist;
expect(countComponent).to.exist;
// TODO: enable this part of the test
/*
expect(buttonComponent.el.innerHTML).to.contain('0');
expect(buttonComponent.el.className).to.equal('app-button app-button-small');
expect(buttonComponent.el.innerHTML).to.contain("0");
expect(buttonComponent.el.className).to.equal(
"app-button app-button-small"
);
buttonComponent.setSize('large');
buttonComponent.setSize("large");
buttonComponent.update();
expect(buttonComponent.el.innerHTML).to.contxain('0');
expect(buttonComponent.el.className).to.equal('app-button app-button-large');
expect(buttonComponent.el.innerHTML).to.contain("0");
expect(buttonComponent.el.className).to.equal(
"app-button app-button-large"
);
debugger;
countComponent.increment();
countComponent.update();
expect(buttonComponent.el.innerHTML).to.contain('1');
expect(buttonComponent.el.innerHTML).to.contain("1");
buttonComponent.setSize('small');
buttonComponent.setSize("small");
buttonComponent.update();
expect(buttonComponent.el.innerHTML).to.contain('1');
expect(buttonComponent.el.className).to.equal('app-button app-button-small');
*/
expect(buttonComponent.el.innerHTML).to.contain("1");
expect(buttonComponent.el.className).to.equal(
"app-button app-button-small"
);
};

View File

@ -0,0 +1,3 @@
class {}
<${input}/>

View File

@ -0,0 +1,11 @@
class {
onCreate() {
this.state = { count:0 };
}
increment() {
this.state.count++;
}
}
-- ${state.count}

View File

@ -0,0 +1,5 @@
class {}
<for(i from 1 to 3)>
<${input}/>
</for>

View File

@ -0,0 +1,7 @@
class {}
<list>
<container key="container[]">
<counter key="counter[]"/>
</container>
</list>

View File

@ -0,0 +1,33 @@
var expect = require("chai").expect;
module.exports = function(helpers) {
var root = helpers.mount(require.resolve("./index"));
var counters = root.getComponents("counter");
var containers = root.getComponents("container");
expect(helpers.targetEl.textContent).to.equal("000");
counters[0].increment();
counters[0].update();
counters[1].increment();
counters[1].increment();
counters[1].update();
counters[2].increment();
counters[2].increment();
counters[2].increment();
counters[2].update();
expect(helpers.targetEl.textContent).to.equal("123");
containers[1].forceUpdate();
containers[1].update();
containers[2].forceUpdate();
containers[2].update();
expect(helpers.targetEl.textContent).to.equal("123");
counters[2].increment();
counters[2].update();
expect(helpers.targetEl.textContent).to.equal("124");
};

View File

@ -4,6 +4,4 @@ class {
}
}
<div>
<counter key="counter" if(!state.hideOwnCounter)/>|<include(input.renderBody)/>
</div>
<counter key="counter" if(!state.hideOwnCounter)/><span>|</span><${input}/>

View File

@ -33,13 +33,18 @@ function runClientTest(fixture) {
if (isAsync) {
testFunc(helpers, cleanupAndFinish);
} else {
testFunc(helpers);
cleanupAndFinish();
let err;
try {
testFunc(helpers);
} catch (e) {
err = e;
}
cleanupAndFinish(err);
}
function cleanupAndFinish(err) {
// Cache components for use in hydrate run.
context.rendered = helpers.rendered;
if (!err) context.rendered = helpers.rendered;
helpers.instances.forEach(instance => instance.destroy());
helpers.targetEl.innerHTML = "";
done(err);

View File

@ -32,7 +32,7 @@ function render(input, out, __component, component, state) {
out.w("</ul>");
var __key5 = __component.___nextKey("preservedP");
var __key5 = __component.___nextKey("@preservedP");
out.w("<p>");

View File

@ -20,7 +20,7 @@ var marko_template = module.exports = require("marko/src/vdom").t(__filename),
marko_node0 = marko_createElement("HEAD", null, "1", null, 1, 0, {
i: marko_const_nextId()
})
.e("TITLE", null, "2", null, 1)
.e("TITLE", null, null, null, 1)
.t("Hello"),
marko_node1 = marko_createElement("H1", null, "4", null, 1, 0, {
i: marko_const_nextId()

View File

@ -18,7 +18,7 @@ var marko_template = module.exports = require("marko/src/vdom").t(__filename),
function render(input, out, __component, component, state) {
var data = input;
out.e("H1", null, input.myStartKey, component, 0);
out.e("H1", null, "@" + input.myStartKey, component, 0);
my_component_tag({}, out, __component, "0");
}

View File

@ -13,9 +13,9 @@ var marko_template = module.exports = require("marko/src/vdom").t(__filename),
function render(input, out, __component, component, state) {
var data = input;
out.e("H1", null, input.myStartKey, component, 0);
out.e("H1", null, "@" + input.myStartKey, component, 0);
out.e("DIV", null, input.myEndKey, component, 0);
out.e("DIV", null, "@" + input.myEndKey, component, 0);
}
marko_template._ = marko_renderer(render, {

View File

@ -13,10 +13,10 @@ var marko_template = module.exports = require("marko/src/vdom").t(__filename),
marko_createElement = marko_helpers.e,
marko_const = marko_helpers.const,
marko_const_nextId = marko_const("339bea"),
marko_node0 = marko_createElement("H1", null, "myStart", null, 0, 0, {
marko_node0 = marko_createElement("H1", null, "@myStart", null, 0, 0, {
i: marko_const_nextId()
}),
marko_node1 = marko_createElement("DIV", null, "myEnd", null, 0, 0, {
marko_node1 = marko_createElement("DIV", null, "@myEnd", null, 0, 0, {
i: marko_const_nextId()
});

View File

@ -6,7 +6,7 @@ describe(path.basename(__dirname), function() {
var app = window.app;
app.forceUpdate();
app.update();
expect(app.getEl().outerHTML).to.equal(undefined);
expect(app.getEl()).to.equal(undefined);
app.input = { show: true };
app.forceUpdate();
app.update();

View File

@ -2,7 +2,7 @@ var path = require("path");
var expect = require("chai").expect;
describe(path.basename(__dirname), function() {
it.fails("should update correctly", function() {
it("should update correctly", function() {
var component = window.component;
var $button = component.getEl("button");
expect($button.textContent).to.eql("button label");
@ -10,7 +10,6 @@ describe(path.basename(__dirname), function() {
$button.click();
component.update();
expect($button.textContent).to.eql("button label test");
}).details =
"issue #912";
expect($button.textContent).to.eql("button labeltest");
});
});

View File

@ -0,0 +1,2 @@
class {}
<div/>

View File

@ -0,0 +1,5 @@
module.exports = {
onMount() {
window.component = this;
}
};

View File

@ -0,0 +1,6 @@
$ var promise = new Promise(resolve => setTimeout(resolve, 100));
<await(_ from promise) client-reorder>
<div key="div"/>
<app-child key="child"/>
</await>

View File

@ -0,0 +1,16 @@
<marko no-browser-rerender />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title> Marko Test</title>
</head>
<body>
<div id="test"/>
<div id="mocha"/>
<div id="testsTarget" />
<app-hello />
</body>
</html>

View File

@ -0,0 +1,10 @@
var path = require("path");
var expect = require("chai").expect;
describe(path.basename(__dirname), function() {
it("should initialize components correctly across async boundaries", function(done) {
expect(window.component.getEl("div")).to.not.equal(undefined);
expect(window.component.getComponent("child")).to.not.equal(undefined);
done();
});
});

View File

@ -18,18 +18,24 @@ function run(fixture) {
var testFile = resolve("tests.js");
var templateFile = resolve("template.marko");
var template = require(templateFile);
return template.render({}).then(function(html) {
var browser = createBrowserWithMarko(__dirname, String(html), {
beforeParse(window, browser) {
browser.require("../../components");
browser.require(templateFile);
}
return template
.render({})
.then(function(html) {
var browser = createBrowserWithMarko(__dirname, String(html), {
beforeParse(window, browser) {
browser.require("../../components");
browser.window.$initComponents();
browser.require(templateFile);
}
});
after(function() {
browser.window.close();
});
return browser;
})
.then(function(browser) {
browser.window.document.close();
browser.require(testFile);
});
after(function() {
browser.window.close();
});
browser.require(testFile);
browser.window.$initComponents();
});
});
}

View File

@ -1 +1 @@
<!--M#s0--><div class="a">Hello Frank</div><!--M/s0-->
<!--M#s0--><div class="a">Hello Frank</div><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><div class="a">Hello Frank</div><!--M/s0-->
<!--M#s0--><div class="a">Hello Frank</div><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><div class="b">Hello Frank</div><!--M/s0-->
<!--M#s0--><div class="b">Hello Frank</div><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><div class="b">Hello Jane</div><!--M/s0-->
<!--M#s0--><div class="b">Hello Jane</div><!--M/-->

View File

@ -42,9 +42,6 @@ autotest("fixtures", fixture => {
encoding: "utf8"
});
var parentNode = fromNode;
var startNode = fromNode.firstChild;
var endNode = null;
var toNode = targetVEl;
var doc = fromDocument;
var componentsContext = {
@ -55,14 +52,7 @@ autotest("fixtures", fixture => {
}
};
morphdom(
parentNode,
startNode,
endNode,
toNode,
doc,
componentsContext
);
morphdom(fromNode, toNode, doc, componentsContext);
var actualHTML = serializeNode(fromNode);
snapshot(actualHTML, ".html");

View File

@ -1 +1 @@
<html><body><div class="inner"><div class="inner-inner"><!--M#s0-6--><div>Hello inner-inner</div><!--M/s0-6--></div><!--M#s0-7--><div>Hello inner</div><!--M/s0-7--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6",0,{"name":"inner-inner"},{"f":1}],["s0-7",0,{"name":"inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await-beginAsync/components/hello/index.marko"]})||w.$components})()</script><!--M#s0-8--><div>Hello outer</div><!--M/s0-8--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-8",0,{"name":"outer"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await-beginAsync/components/hello/index.marko"]})||w.$components})()</script></body></html>
<html><body><div class="inner"><div class="inner-inner"><!--M^s0-6 s0 6--><div>Hello inner-inner</div><!--M/--></div><!--M^s0-7 s0 7--><div>Hello inner</div><!--M/--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6",0,{"name":"inner-inner"},{"f":1}],["s0-7",0,{"name":"inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await-beginAsync/components/hello/index.marko"]})||w.$components})()</script><!--M^s0-8 s0 8--><div>Hello outer</div><!--M/--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-8",0,{"name":"outer"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await-beginAsync/components/hello/index.marko"]})||w.$components})()</script></body></html>

View File

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

View File

@ -0,0 +1 @@
exports.skip_vdom = "server-rendered vdom comment boundaries are not a concern";

View File

@ -1 +1 @@
<html><body><div class="inner"><div class="inner-inner"><!--M#s0-6--><div>Hello inner-inner</div><!--M/s0-6--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6",0,{"name":"inner-inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script><!--M#s0-7--><div>Hello inner</div><!--M/s0-7--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-7",0,{"name":"inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script><!--M#s0-8--><div>Hello outer</div><!--M/s0-8--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-8",0,{"name":"outer"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script></body></html>
<html><body><div class="inner"><div class="inner-inner"><!--M^s0-6 s0 6--><div>Hello inner-inner</div><!--M/--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6",0,{"name":"inner-inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script><!--M^s0-7 s0 7--><div>Hello inner</div><!--M/--></div><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-7",0,{"name":"inner"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script><!--M^s0-8 s0 8--><div>Hello outer</div><!--M/--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-8",0,{"name":"outer"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures-async/components-await/components/hello/index.marko"]})||w.$components})()</script></body></html>

View File

@ -1 +1 @@
<!--M#s0--><div id="s0-buttonDescription">Submit the form thing</div><button aria-described-by="s0-buttonDescription">Submit</button><!--M/s0-->
<!--M#s0--><div id="s0-buttonDescription">Submit the form thing</div><button aria-described-by="s0-buttonDescription">Submit</button><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><div id="s0-buttonDescription">Submit the form thing</div><button aria-described-by="s0-buttonDescription">Submit</button><!--M/s0-->
<!--M#s0--><div id="s0-buttonDescription">Submit the form thing</div><button aria-described-by="s0-buttonDescription">Submit</button><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><div><input id="s0-checkbox"><label for="s0-checkbox"></label></div><!--M/s0-->
<!--M#s0--><div><input id="s0-checkbox"><label for="s0-checkbox"></label></div><!--M/-->

View File

@ -1 +1 @@
<div><!--M#s0-1--><div class="hello">Hello Frank!</div><!--M/s0-1--></div>
<div><!--M^s0-1 s0 1--><div class="hello">Hello Frank!</div><!--M/--></div>

View File

@ -0,0 +1 @@
exports.skip_vdom = "server-rendered vdom comment boundaries are not a concern";

View File

@ -1 +1 @@
<!--M#s0--><div><input id="s0-checkbox"><label for="s0-checkbox"></label></div><!--M/s0-->
<!--M#s0--><div><input id="s0-checkbox"><label for="s0-checkbox"></label></div><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><label for="s0-submitButton">Submit</label><button id="s0-submitButton">Submit</button><!--M/s0-->
<!--M#s0--><label for="s0-submitButton">Submit</label><button id="s0-submitButton">Submit</button><!--M/-->

View File

@ -1 +1 @@
<!--M#s0--><!DOCTYPE html><html lang="en"><body>Hello <script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0",0,{"name":"World"},{"f":1,"s":{"foo-0":"bar\u2028","foo-1":"bar\u2029","foo-2":"\u2028bar\u2029","foo-3":"Hello \u003C/script> \u2028bar\u2029"}}]],"t":["/marko-test$1.0.0/render/fixtures/component-safe-json/template.marko"]})||w.$components})()</script></body></html><!--M/s0-->
<!--M#s0--><!DOCTYPE html><html lang="en"><body>Hello <script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0",0,{"name":"World"},{"f":1,"s":{"foo-0":"bar\u2028","foo-1":"bar\u2029","foo-2":"\u2028bar\u2029","foo-3":"Hello \u003C/script> \u2028bar\u2029"}}]],"t":["/marko-test$1.0.0/render/fixtures/component-safe-json/template.marko"]})||w.$components})()</script></body></html><!--M/-->

View File

@ -1 +1 @@
<html><head>Components</head><body><!--M#s0-3--><div><h1>foo1</h1><div><h1>bar1</h1></div><div><h1>bar2</h1></div><div><h1>foo-split1</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div><div><h1>foo-split2</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div></div><!--M/s0-3--><!--M#s0-4--><div><h1>foo2</h1><div><h1>bar1</h1></div><div><h1>bar2</h1></div><div><h1>foo-split1</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div><div><h1>foo-split2</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div></div><!--M/s0-4--><!--M^s0-5--><div><h1>split1</h1><!--M^s0-5-split-child1--><div><h1>split-child1</h1></div><!--M/s0-5-split-child1--><!--M^s0-5-split-child2--><div><h1>split-child2</h1></div><!--M/s0-5-split-child2--></div><!--M/s0-5--><!--M^s0-6--><div><h1>split2</h1><!--M^s0-6-split-child1--><div><h1>split-child1</h1></div><!--M/s0-6-split-child1--><!--M^s0-6-split-child2--><div><h1>split-child2</h1></div><!--M/s0-6-split-child2--></div><!--M/s0-6--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6-split-child2",0,{"name":"split-child2"},{}],["s0-6-split-child1",0,{"name":"split-child1"},{}],["s0-6",1,{"name":"split2"},{}],["s0-5-split-child2",0,{"name":"split-child2"},{}],["s0-5-split-child1",0,{"name":"split-child1"},{}],["s0-5",1,{"name":"split1"},{}],["s0-4",2,{"name":"foo2"},{"f":1}],["s0-3",2,{"name":"foo1"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures/components/components/split/components/split-child/component-browser","/marko-test$1.0.0/render/fixtures/components/components/split/component-browser","/marko-test$1.0.0/render/fixtures/components/components/foo/index.marko"]})||w.$components})()</script></body></html>
<html><head>Components</head><body><!--M^s0-3 s0 3--><div><h1>foo1</h1><div><h1>bar1</h1></div><div><h1>bar2</h1></div><div><h1>foo-split1</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div><div><h1>foo-split2</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div></div><!--M/--><!--M^s0-4 s0 4--><div><h1>foo2</h1><div><h1>bar1</h1></div><div><h1>bar2</h1></div><div><h1>foo-split1</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div><div><h1>foo-split2</h1><div><h1>split-child1</h1></div><div><h1>split-child2</h1></div></div></div><!--M/--><!--M^s0-5 s0 5--><div><h1>split1</h1><!--M^s0-5-split-child1 s0-5 split-child1--><div><h1>split-child1</h1></div><!--M/--><!--M^s0-5-split-child2 s0-5 split-child2--><div><h1>split-child2</h1></div><!--M/--></div><!--M/--><!--M^s0-6 s0 6--><div><h1>split2</h1><!--M^s0-6-split-child1 s0-6 split-child1--><div><h1>split-child1</h1></div><!--M/--><!--M^s0-6-split-child2 s0-6 split-child2--><div><h1>split-child2</h1></div><!--M/--></div><!--M/--><script>(function(){var w=window;w.$components=(w.$components||[]).concat({"w":[["s0-6-split-child2",0,{"name":"split-child2"},{}],["s0-6-split-child1",0,{"name":"split-child1"},{}],["s0-6",1,{"name":"split2"},{}],["s0-5-split-child2",0,{"name":"split-child2"},{}],["s0-5-split-child1",0,{"name":"split-child1"},{}],["s0-5",1,{"name":"split1"},{}],["s0-4",2,{"name":"foo2"},{"f":1}],["s0-3",2,{"name":"foo1"},{"f":1}]],"t":["/marko-test$1.0.0/render/fixtures/components/components/split/components/split-child/component-browser","/marko-test$1.0.0/render/fixtures/components/components/split/component-browser","/marko-test$1.0.0/render/fixtures/components/components/foo/index.marko"]})||w.$components})()</script></body></html>