mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
* 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
405 lines
11 KiB
JavaScript
405 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const Node = require("../../Node");
|
|
const vdomUtil = require("../../../util/vdom");
|
|
|
|
const FLAG_IS_SVG = 1;
|
|
const FLAG_IS_TEXTAREA = 2;
|
|
const FLAG_SIMPLE_ATTRS = 4;
|
|
// const FLAG_PRESERVE = 8;
|
|
// const FLAG_CUSTOM_ELEMENT = 16;
|
|
|
|
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;
|
|
var lastArg;
|
|
|
|
for (var i = length - 1; i >= 0; i--) {
|
|
var arg = createArgs[i];
|
|
if (arg) {
|
|
lastArg = arg;
|
|
} else {
|
|
if (lastArg != null) {
|
|
if (i === INDEX_FLAGS) {
|
|
// Use a literal 0 for the flags
|
|
createArgs[i] = builder.literal(0);
|
|
} else {
|
|
createArgs[i] = builder.literalNull();
|
|
}
|
|
} else {
|
|
length--;
|
|
}
|
|
}
|
|
}
|
|
|
|
createArgs.length = length;
|
|
return createArgs;
|
|
}
|
|
|
|
const MAYBE_SVG = {
|
|
a: true,
|
|
script: true,
|
|
style: true
|
|
};
|
|
|
|
const SIMPLE_ATTRS = {
|
|
class: true,
|
|
style: true,
|
|
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");
|
|
this.tagName = def.tagName;
|
|
this.isStatic = def.isStatic;
|
|
this.isAttrsStatic = def.isAttrsStatic;
|
|
this.isHtmlOnly = def.isHtmlOnly;
|
|
this.attributes = def.attributes;
|
|
this.properties = def.properties;
|
|
this.body = def.body;
|
|
this.dynamicAttributes = def.dynamicAttributes;
|
|
this.key = def.key;
|
|
this.runtimeFlags = def.runtimeFlags;
|
|
this.isAutoKeyed = def.isAutoKeyed;
|
|
|
|
this.isSVG = false;
|
|
this.isTextArea = false;
|
|
this.hasAttributes = false;
|
|
this.hasSimpleAttrs = false; // This will be set to true if the HTML element
|
|
// only attributes in the following set:
|
|
// ['id', 'style', 'class']
|
|
|
|
this.isChild = false;
|
|
this.createElementId = undefined;
|
|
this.attributesArg = undefined;
|
|
this.propertiesArg = undefined;
|
|
}
|
|
|
|
generateCode(codegen) {
|
|
let context = codegen.context;
|
|
let builder = codegen.builder;
|
|
|
|
vdomUtil.registerOptimizer(context);
|
|
|
|
let tagName = this.tagName;
|
|
|
|
if (tagName.type === "Literal" && typeof tagName.value === "string") {
|
|
let tagDef = context.getTagDef(tagName.value);
|
|
if (tagDef) {
|
|
if (tagDef.htmlType === "svg") {
|
|
this.isSVG = true;
|
|
} else {
|
|
if (MAYBE_SVG[tagName.value] && context.isFlagSet("SVG")) {
|
|
this.isSVG = true;
|
|
} else {
|
|
this.tagName = tagName = builder.literal(
|
|
tagName.value.toUpperCase()
|
|
);
|
|
|
|
if (tagName.value === "TEXTAREA") {
|
|
this.isTextArea = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.isLiteralTag = true;
|
|
} else if (context.isFlagSet("SVG")) {
|
|
this.isSVG = true;
|
|
}
|
|
|
|
let attributes = this.attributes;
|
|
let properties = this.properties;
|
|
let dynamicAttributes = this.dynamicAttributes;
|
|
|
|
let attributesArg = null;
|
|
|
|
var hasNamedAttributes = false;
|
|
var hasDynamicAttributes =
|
|
dynamicAttributes != null && dynamicAttributes.length !== 0;
|
|
var hasSpreadAttributes = false;
|
|
|
|
var hasSimpleAttrs = true;
|
|
|
|
if (properties && properties.noupdate) {
|
|
// Preserving attributes requires extra logic that we cannot
|
|
// shortcircuit
|
|
hasSimpleAttrs = false;
|
|
}
|
|
|
|
if (attributes != null && attributes.length !== 0) {
|
|
let explicitAttrs = null;
|
|
let attrs = [];
|
|
let addAttr = function(name, value) {
|
|
hasNamedAttributes = true;
|
|
|
|
if (!SIMPLE_ATTRS[name]) {
|
|
hasSimpleAttrs = false;
|
|
}
|
|
|
|
if (!explicitAttrs) {
|
|
explicitAttrs = {};
|
|
}
|
|
|
|
if (value.type === "Literal") {
|
|
let literalValue = value.value;
|
|
if (literalValue == null || literalValue === false) {
|
|
return;
|
|
} else if (typeof literalValue === "number") {
|
|
value.value = literalValue.toString();
|
|
}
|
|
} else if (value.type === "AttributePlaceholder") {
|
|
value = codegen.builder.functionCall(
|
|
context.helper("str"),
|
|
[value]
|
|
);
|
|
}
|
|
|
|
explicitAttrs[name] = value;
|
|
};
|
|
|
|
attributes.forEach((attr, i) => {
|
|
// deprecated
|
|
if (!attr.name && !attr.spread) {
|
|
return;
|
|
}
|
|
|
|
if (attr.spread) {
|
|
let isFirstOfMany = i === 0 && attributes.length > 1;
|
|
if (explicitAttrs || isFirstOfMany) {
|
|
attrs.push(builder.literal(explicitAttrs || {}));
|
|
}
|
|
attrs.push(
|
|
codegen.builder.functionCall(context.helper("attrs"), [
|
|
attr.value
|
|
])
|
|
);
|
|
explicitAttrs = null;
|
|
hasSpreadAttributes = true;
|
|
} else {
|
|
let value = attr.value;
|
|
|
|
if (value == null) {
|
|
value = builder.literal(true);
|
|
}
|
|
|
|
addAttr(attr.name, value);
|
|
}
|
|
});
|
|
|
|
if (explicitAttrs) {
|
|
attrs.push(builder.literal(explicitAttrs));
|
|
}
|
|
|
|
attributesArg =
|
|
attrs.length > 1
|
|
? builder.functionCall(context.helper("assign"), attrs)
|
|
: attrs[0];
|
|
}
|
|
|
|
// deprecated
|
|
if (hasDynamicAttributes) {
|
|
dynamicAttributes.forEach(attrs => {
|
|
if (attributesArg) {
|
|
let mergeVar = context.helper("merge");
|
|
attributesArg = builder.functionCall(mergeVar, [
|
|
attributesArg, // Input props from the attributes take precedence
|
|
attrs
|
|
]);
|
|
} else {
|
|
attributesArg = attrs;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (
|
|
!this.isAttrsStatic &&
|
|
hasNamedAttributes &&
|
|
hasSimpleAttrs &&
|
|
!hasDynamicAttributes &&
|
|
!hasSpreadAttributes
|
|
) {
|
|
this.hasSimpleAttrs = true;
|
|
}
|
|
|
|
this.hasAttributes = hasNamedAttributes || hasDynamicAttributes;
|
|
|
|
this.attributesArg = attributesArg;
|
|
|
|
this.properties = builder.literal(this.properties || {});
|
|
|
|
return this;
|
|
}
|
|
|
|
walk(walker) {
|
|
this.tagName = walker.walk(this.tagName);
|
|
this.attributes = walker.walk(this.attributes);
|
|
this.body = walker.walk(this.body);
|
|
}
|
|
|
|
writeCode(writer) {
|
|
let builder = writer.builder;
|
|
|
|
let body = this.body;
|
|
let attributesArg = this.attributesArg;
|
|
|
|
let tagName = this.tagName;
|
|
|
|
let key = this.key;
|
|
|
|
let childCount = body && body.length;
|
|
|
|
let createArgs = new Array(CREATE_ARGS_COUNT);
|
|
|
|
createArgs[INDEX_TAG_NAME] = tagName;
|
|
|
|
if (attributesArg) {
|
|
createArgs[INDEX_ATTRS] = attributesArg;
|
|
}
|
|
|
|
if (
|
|
key &&
|
|
(!this.isAutoKeyed || !this.isStatic || this.createElementId)
|
|
) {
|
|
createArgs[INDEX_KEY] = key;
|
|
|
|
if (!this.isStatic) {
|
|
createArgs[INDEX_COMPONENT] = builder.identifier("component");
|
|
}
|
|
}
|
|
|
|
if (childCount != null) {
|
|
createArgs[INDEX_CHILD_COUNT] = builder.literal(childCount);
|
|
}
|
|
|
|
var flags = 0;
|
|
|
|
if (this.isSVG) {
|
|
flags |= FLAG_IS_SVG;
|
|
}
|
|
|
|
if (this.isTextArea) {
|
|
flags |= FLAG_IS_TEXTAREA;
|
|
}
|
|
|
|
if (this.hasSimpleAttrs) {
|
|
flags |= FLAG_SIMPLE_ATTRS;
|
|
}
|
|
|
|
if (this.runtimeFlags) {
|
|
flags |= this.runtimeFlags;
|
|
}
|
|
|
|
if (flags) {
|
|
createArgs[INDEX_FLAGS] = builder.literal(flags);
|
|
}
|
|
|
|
let properties = 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
|
|
// undefined elements to a literal null node
|
|
createArgs = finalizeCreateArgs(createArgs, builder);
|
|
|
|
let funcCall;
|
|
|
|
if (this.isChild) {
|
|
writer.write(".");
|
|
|
|
funcCall = builder.functionCall(
|
|
builder.identifier(
|
|
this.isLiteralTag || this.isSVG ? "e" : "ed"
|
|
),
|
|
createArgs
|
|
);
|
|
} else if (this.isStatic && this.createElementId) {
|
|
funcCall = builder.functionCall(this.createElementId, createArgs);
|
|
} else if (this.isHtmlOnly) {
|
|
writer.write("out.");
|
|
funcCall = builder.functionCall(
|
|
builder.identifier(
|
|
this.isLiteralTag || this.isSVG ? "e" : "ed"
|
|
),
|
|
createArgs
|
|
);
|
|
} else {
|
|
writer.write("out.");
|
|
funcCall = builder.functionCall(
|
|
builder.identifier(
|
|
this.isLiteralTag || this.isSVG ? "be" : "bed"
|
|
),
|
|
createArgs
|
|
);
|
|
}
|
|
|
|
writer.write(funcCall);
|
|
|
|
if (body && body.length) {
|
|
writer.incIndent();
|
|
for (let i = 0; i < body.length; i++) {
|
|
let child = body[i];
|
|
child.isChild = true;
|
|
writer.write("\n");
|
|
writer.writeLineIndent();
|
|
writer.write(child);
|
|
}
|
|
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;
|