mirror of
https://github.com/marko-js/marko.git
synced 2026-02-01 16:07:13 +00:00
Custom elements support (#860)
* support rendering into a shadow root * only lowercase the first letter of an event when camel style is used * Don't use let. Allow ShadowRoot in jshint. * for backwards compat, lowercase the entire event when using camel style * define a custom elements's import as a dependency when it is used in a template * when rendering on the server, generate a script tag to assign custom-element properties. * assign attributes to custom-element as properties when actualizing/morphing * add basic test for rendering a custom-element and assigning properties * use flag to mark custom elements * fix brittle version test
This commit is contained in:
parent
5d60824590
commit
c96eca1c70
@ -1,6 +1,7 @@
|
||||
{
|
||||
"predef": [
|
||||
"document"
|
||||
"document",
|
||||
"ShadowRoot"
|
||||
],
|
||||
"node" : true,
|
||||
"esnext": true,
|
||||
|
||||
@ -23,6 +23,12 @@ const markoPkgVersion = require('../../package.json').version;
|
||||
const rootDir = path.join(__dirname, '../');
|
||||
const isDebug = require('../build.json').isDebug;
|
||||
|
||||
// const FLAG_IS_SVG = 1;
|
||||
// const FLAG_IS_TEXTAREA = 2;
|
||||
// const FLAG_SIMPLE_ATTRS = 4;
|
||||
// const FLAG_PRESERVE = 8;
|
||||
const FLAG_CUSTOM_ELEMENT = 16;
|
||||
|
||||
const FLAG_PRESERVE_WHITESPACE = 'PRESERVE_WHITESPACE';
|
||||
|
||||
function getTaglibPath(taglibPath) {
|
||||
@ -89,6 +95,7 @@ const helpers = {
|
||||
'loadTemplate': { module: 'marko/runtime/helper-loadTemplate' },
|
||||
'mergeNestedTagsHelper': { module: 'marko/runtime/helper-mergeNestedTags' },
|
||||
'merge': { module: 'marko/runtime/helper-merge' },
|
||||
'propsForPreviousNode': 'p',
|
||||
'renderer': {
|
||||
module: 'marko/components/helpers',
|
||||
method: 'r'
|
||||
@ -484,23 +491,27 @@ class CompileContext extends EventEmitter {
|
||||
} else {
|
||||
if (typeof tagName === 'string') {
|
||||
tagDef = taglibLookup.getTag(tagName);
|
||||
if (!tagDef &&
|
||||
!this.isMacro(tagName) &&
|
||||
tagName.indexOf(':') === -1 &&
|
||||
!htmlElements.isRegisteredElement(tagName, this.dirname) &&
|
||||
!this.ignoreUnrecognizedTags) {
|
||||
if (!tagDef && !this.isMacro(tagName) && tagName.indexOf(':') === -1) {
|
||||
var customElement = htmlElements.getRegisteredElement(tagName, this.dirname);
|
||||
if (customElement) {
|
||||
elNode.customElement = customElement;
|
||||
elNode.addRuntimeFlag(FLAG_CUSTOM_ELEMENT);
|
||||
|
||||
if (this._parsingFinished) {
|
||||
this.addErrorUnrecognizedTag(tagName, elNode);
|
||||
} else {
|
||||
// We don't throw an error right away since the tag
|
||||
// may be a macro that gets registered later
|
||||
this.unrecognizedTags.push({
|
||||
node: elNode,
|
||||
tagName: tagName
|
||||
});
|
||||
if (customElement.import) {
|
||||
this.addDependency(this.getRequirePath(customElement.import));
|
||||
}
|
||||
} else if (!this.ignoreUnrecognizedTags) {
|
||||
if (this._parsingFinished) {
|
||||
this.addErrorUnrecognizedTag(tagName, elNode);
|
||||
} else {
|
||||
// We don't throw an error right away since the tag
|
||||
// may be a macro that gets registered later
|
||||
this.unrecognizedTags.push({
|
||||
node: elNode,
|
||||
tagName: tagName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,8 +37,7 @@ module.exports = function generateCode(node, codegen) {
|
||||
var bodyOnlyIf = node.bodyOnlyIf;
|
||||
var dynamicAttributes = node.dynamicAttributes;
|
||||
var selfClosed = node.selfClosed === true;
|
||||
|
||||
|
||||
var isCustomElement = node.customElement;
|
||||
|
||||
if (hasBody) {
|
||||
body = codegen.generateCode(body);
|
||||
@ -53,14 +52,15 @@ module.exports = function generateCode(node, codegen) {
|
||||
|
||||
var startTag = new StartTag({
|
||||
tagName: tagName,
|
||||
attributes: attributes,
|
||||
attributes: isCustomElement ? null : attributes,
|
||||
properties: properties,
|
||||
argument: argument,
|
||||
selfClosed: selfClosed,
|
||||
dynamicAttributes: dynamicAttributes
|
||||
dynamicAttributes: isCustomElement ? null : dynamicAttributes
|
||||
});
|
||||
|
||||
var endTag;
|
||||
var propertiesScript = [];
|
||||
|
||||
if (!openTagOnly) {
|
||||
endTag = new EndTag({
|
||||
@ -68,15 +68,27 @@ module.exports = function generateCode(node, codegen) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isCustomElement && attributes && attributes.length) {
|
||||
propertiesScript = builder.functionCall(
|
||||
codegen.context.helper('propsForPreviousNode'),
|
||||
[
|
||||
builder.objectExpression(attributes.map((attr) =>
|
||||
builder.property(builder.identifier(attr.name), attr.value)
|
||||
)),
|
||||
builder.identifier('out')
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (bodyOnlyIf) {
|
||||
var startIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
|
||||
startTag
|
||||
]);
|
||||
|
||||
var endIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
|
||||
endTag
|
||||
endTag,
|
||||
propertiesScript
|
||||
]);
|
||||
|
||||
return [
|
||||
startIf,
|
||||
body,
|
||||
@ -84,12 +96,16 @@ module.exports = function generateCode(node, codegen) {
|
||||
];
|
||||
} else {
|
||||
if (openTagOnly) {
|
||||
return codegen.generateCode(startTag);
|
||||
return [
|
||||
codegen.generateCode(startTag),
|
||||
propertiesScript
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
startTag,
|
||||
body,
|
||||
endTag
|
||||
endTag,
|
||||
propertiesScript
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,7 @@ const FLAG_IS_SVG = 1;
|
||||
const FLAG_IS_TEXTAREA = 2;
|
||||
const FLAG_SIMPLE_ATTRS = 4;
|
||||
// const FLAG_PRESERVE = 8;
|
||||
// const FLAG_COMPONENT_START_NODE = 16;
|
||||
// const FLAG_COMPONENT_END_NODE = 32;
|
||||
// const FLAG_CUSTOM_ELEMENT = 16;
|
||||
|
||||
let CREATE_ARGS_COUNT = 0;
|
||||
const INDEX_TAG_NAME = CREATE_ARGS_COUNT++;
|
||||
|
||||
@ -5,6 +5,7 @@ var lassoCachingFS = require('lasso-caching-fs');
|
||||
var fs = require('fs');
|
||||
var stripJsonComments = require('strip-json-comments');
|
||||
var fsReadOptions = { encoding: 'utf8' };
|
||||
var modules = require('../modules');
|
||||
|
||||
function parseJSONFile(path) {
|
||||
var json = fs.readFileSync(path, fsReadOptions);
|
||||
@ -27,7 +28,10 @@ function loadTags(file) {
|
||||
if (raw.hasOwnProperty(k)) {
|
||||
if (k.charAt(0) === '<' && k.charAt(k.length - 1) === '>') {
|
||||
var tagName = k.substring(1, k.length - 1);
|
||||
tags[tagName] = true;
|
||||
var tag = tags[tagName] = raw[k];
|
||||
if (tag.import && tag.import[0] === '.') {
|
||||
tag.import = modules.resolveFrom(path.dirname(file), tag.import);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -46,7 +50,7 @@ function getPackageRootDir(dirname) {
|
||||
}
|
||||
}
|
||||
|
||||
function isRegisteredElement(tagName, dir) {
|
||||
function getRegisteredElement(tagName, dir) {
|
||||
var packageRootDir = getPackageRootDir(dir);
|
||||
|
||||
var currentDir = dir;
|
||||
@ -60,19 +64,16 @@ function isRegisteredElement(tagName, dir) {
|
||||
}
|
||||
|
||||
if (tags[tagName]) {
|
||||
return true;
|
||||
return tags[tagName];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var parentDir = path.dirname(currentDir);
|
||||
if (!parentDir || parentDir === currentDir || parentDir === packageRootDir) {
|
||||
break;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
exports.isRegisteredElement = isRegisteredElement;
|
||||
exports.getRegisteredElement = getRegisteredElement;
|
||||
@ -51,7 +51,7 @@ function delegateEvent(node, target, event) {
|
||||
}
|
||||
|
||||
function attachBubbleEventListeners(doc) {
|
||||
var body = doc.body;
|
||||
var body = doc.body || doc;
|
||||
// Here's where we handle event delegation using our own mechanism
|
||||
// for delegating events. For each event that we have white-listed
|
||||
// as supporting bubble, we will attach a listener to the root
|
||||
|
||||
@ -168,16 +168,17 @@ module.exports = function handleComponentEvents() {
|
||||
addCustomEventListener(this, eventType, targetMethod, extraArgs);
|
||||
} else {
|
||||
// We are adding an event listener for a DOM event (not a custom event)
|
||||
//
|
||||
if (eventType.startsWith('-')) {
|
||||
// Remove the leading dash.
|
||||
// Example: w-on-before-show → before-show
|
||||
// Remove the leading dash. Preserve casing.
|
||||
// Example: on-before-show → before-show
|
||||
// Example: on-CAPS-event → CAPS-event
|
||||
eventType = eventType.substring(1);
|
||||
} else {
|
||||
// Lowercase the string
|
||||
// Example: onMouseOver → mouseover
|
||||
eventType = eventType.toLowerCase();
|
||||
}
|
||||
|
||||
// Normalize DOM event types to be all lower case
|
||||
eventType = eventType.toLowerCase();
|
||||
|
||||
// Node is for an HTML element so treat the event as a DOM event
|
||||
var willBubble = isBubbleEvent(eventType);
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ var COMPONENT_NODE = 2;
|
||||
// var FLAG_IS_TEXTAREA = 2;
|
||||
// var FLAG_SIMPLE_ATTRS = 4;
|
||||
var FLAG_PRESERVE = 8;
|
||||
// var FLAG_CUSTOM_ELEMENT = 16;
|
||||
|
||||
function compareNodeNames(fromEl, toEl) {
|
||||
return fromEl.___nodeName === toEl.___nodeName;
|
||||
|
||||
@ -75,5 +75,6 @@ domInsert(
|
||||
return renderResult.getNode(referenceEl.ownerDocument);
|
||||
},
|
||||
function afterInsert(renderResult, referenceEl) {
|
||||
return renderResult.afterInsert(referenceEl.ownerDocument);
|
||||
var isShadow = typeof ShadowRoot === 'function' && referenceEl instanceof ShadowRoot;
|
||||
return renderResult.afterInsert(isShadow ? referenceEl : referenceEl.ownerDocument);
|
||||
});
|
||||
|
||||
@ -170,3 +170,33 @@ function classList(arg) {
|
||||
var commonHelpers = require('../helpers');
|
||||
extend(exports, commonHelpers);
|
||||
exports.cl = classList;
|
||||
|
||||
|
||||
/**
|
||||
* Internal helper method to insert a script tag that assigns properties
|
||||
* to the dom node the precede it.
|
||||
*/
|
||||
var escapeScript = exports.xs;
|
||||
var assignPropsFunction = `
|
||||
function ap_(p) {
|
||||
var s = document.currentScript;
|
||||
Object.assign(s.previousSibling, p);
|
||||
s.parentNode.removeChild(s);
|
||||
}
|
||||
`.replace(/\s+/g, ' ')
|
||||
.replace(/([\W]) (.)/g, '$1$2')
|
||||
.replace(/(.) ([\W])/g, '$1$2')
|
||||
.trim();
|
||||
exports.p = function propsForPreviousNode(props, out) {
|
||||
var cspNonce = out.global.cspNonce;
|
||||
var nonceAttr = cspNonce ? ' nonce='+JSON.stringify(cspNonce) : '';
|
||||
|
||||
out.w('<script' + nonceAttr + '>');
|
||||
|
||||
if (!out.global.assignPropsFunction) {
|
||||
out.w(assignPropsFunction);
|
||||
out.global.assignPropsFunction = true;
|
||||
}
|
||||
|
||||
out.w('ap_(' + escapeScript(JSON.stringify(props)) + ');</script>');
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ var FLAG_IS_SVG = 1;
|
||||
var FLAG_IS_TEXTAREA = 2;
|
||||
var FLAG_SIMPLE_ATTRS = 4;
|
||||
// var FLAG_PRESERVE = 8;
|
||||
var FLAG_CUSTOM_ELEMENT = 16;
|
||||
|
||||
var defineProperty = Object.defineProperty;
|
||||
|
||||
@ -155,28 +156,32 @@ VElement.prototype = {
|
||||
doc.createElementNS(namespaceURI, tagName) :
|
||||
doc.createElement(tagName);
|
||||
|
||||
for (var attrName in attributes) {
|
||||
var attrValue = attributes[attrName];
|
||||
if (flags & FLAG_CUSTOM_ELEMENT) {
|
||||
Object.assign(el, attributes);
|
||||
} else {
|
||||
for (var attrName in attributes) {
|
||||
var attrValue = attributes[attrName];
|
||||
|
||||
if (attrValue !== false && attrValue != null) {
|
||||
var type = typeof attrValue;
|
||||
if (attrValue !== false && attrValue != null) {
|
||||
var type = typeof attrValue;
|
||||
|
||||
if (type !== 'string') {
|
||||
// Special attributes aren't copied to the real DOM. They are only
|
||||
// kept in the virtual attributes map
|
||||
attrValue = convertAttrValue(type, attrValue);
|
||||
}
|
||||
if (type !== 'string') {
|
||||
// Special attributes aren't copied to the real DOM. They are only
|
||||
// kept in the virtual attributes map
|
||||
attrValue = convertAttrValue(type, attrValue);
|
||||
}
|
||||
|
||||
if (attrName == ATTR_XLINK_HREF) {
|
||||
setAttribute(el, NS_XLINK, ATTR_HREF, attrValue);
|
||||
} else {
|
||||
el.setAttribute(attrName, attrValue);
|
||||
if (attrName == ATTR_XLINK_HREF) {
|
||||
setAttribute(el, NS_XLINK, ATTR_HREF, attrValue);
|
||||
} else {
|
||||
el.setAttribute(attrName, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & FLAG_IS_TEXTAREA) {
|
||||
el.value = this.___valueInternal;
|
||||
if (flags & FLAG_IS_TEXTAREA) {
|
||||
el.value = this.___value;
|
||||
}
|
||||
}
|
||||
|
||||
el.___markoVElement = this;
|
||||
@ -283,12 +288,17 @@ VElement.___morphAttrs = function(fromEl, vFromEl, toEl) {
|
||||
var removePreservedAttributes = VElement.___removePreservedAttributes;
|
||||
|
||||
var fromFlags = vFromEl.___flags;
|
||||
var toFlags = toEl.___flags;
|
||||
|
||||
fromEl.___markoVElement = toEl;
|
||||
|
||||
var attrs = toEl.___attributes;
|
||||
var props = toEl.___properties;
|
||||
|
||||
if (toFlags & FLAG_CUSTOM_ELEMENT) {
|
||||
return Object.assign(fromEl, attrs);
|
||||
}
|
||||
|
||||
var attrName;
|
||||
|
||||
// We use expando properties to associate the previous HTML
|
||||
@ -315,9 +325,6 @@ VElement.___morphAttrs = function(fromEl, vFromEl, toEl) {
|
||||
|
||||
var attrValue;
|
||||
|
||||
var toFlags = toEl.___flags;
|
||||
|
||||
|
||||
if (toFlags & FLAG_SIMPLE_ATTRS && fromFlags & FLAG_SIMPLE_ATTRS) {
|
||||
if (oldAttrs['class'] !== (attrValue = attrs['class'])) {
|
||||
fromEl.className = attrValue;
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
// Compiled using marko@4.5.0-beta.2 - DO NOT EDIT
|
||||
"use strict";
|
||||
|
||||
var marko_template = module.exports = require("marko/src/vdom").t(),
|
||||
components_helpers = require("marko/src/components/helpers"),
|
||||
marko_registerComponent = components_helpers.rc,
|
||||
marko_componentType = marko_registerComponent("/marko-test$1.0.0/autotests/api-compiler/compileForBrowser-write-version-comment.js/template.marko", function() {
|
||||
return module.exports;
|
||||
}),
|
||||
marko_renderer = components_helpers.r,
|
||||
marko_defineComponent = components_helpers.c;
|
||||
|
||||
function render(input, out, __component, component, state) {
|
||||
var data = input;
|
||||
|
||||
out.t("Hello ");
|
||||
|
||||
out.t(data.name);
|
||||
|
||||
out.t("!");
|
||||
}
|
||||
|
||||
marko_template._ = marko_renderer(render, {
|
||||
___implicit: true,
|
||||
___type: marko_componentType
|
||||
});
|
||||
|
||||
marko_template.Component = marko_defineComponent({}, marko_template._);
|
||||
@ -2,27 +2,19 @@ var fs = require('fs');
|
||||
var path = require('path');
|
||||
var markoVersion = require('../../../../package.json').version;
|
||||
|
||||
function _appendMarkoVersionComment(str) {
|
||||
return '// Compiled using marko@' + markoVersion + ' - DO NOT EDIT\n' + str;
|
||||
function getMarkoVersionComment() {
|
||||
return '// Compiled using marko@' + markoVersion + ' - DO NOT EDIT\n';
|
||||
}
|
||||
|
||||
exports.check = function(marko, markoCompiler, expect, helpers, done) {
|
||||
var compiler = require('marko/compiler');
|
||||
var templatePath = path.join(__dirname, 'template.marko');
|
||||
var expectedPath = path.join(__dirname, 'expected.js');
|
||||
|
||||
var templateSrc = fs.readFileSync(templatePath, { encoding: 'utf8' });
|
||||
|
||||
var compiledTemplate = compiler.compileForBrowser(templateSrc, templatePath);
|
||||
var expected = fs.readFileSync(expectedPath, { encoding: 'utf8' });
|
||||
|
||||
compiledTemplate.code = _appendMarkoVersionComment(compiledTemplate.code);
|
||||
expected = _appendMarkoVersionComment(expected);
|
||||
|
||||
var code = compiledTemplate.code;
|
||||
code = code.replace(/marko\/dist\//g, 'marko/src/');
|
||||
|
||||
helpers.compare(code, '.js');
|
||||
expect(compiledTemplate.code).to.include(getMarkoVersionComment());
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<ce-test></ce-test><script>function ap_(p){ var s=document.currentScript;Object.assign(s.previousSibling,p);s.parentNode.removeChild(s);}ap_({"attr":{"foo":"bar"}});</script>
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"<ce-test>": {}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
<ce-test attr={ foo:'bar' }/>
|
||||
Loading…
x
Reference in New Issue
Block a user