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:
Michael Rawlings 2017-09-26 17:37:25 -07:00 committed by GitHub
parent 5d60824590
commit c96eca1c70
16 changed files with 136 additions and 99 deletions

View File

@ -1,6 +1,7 @@
{
"predef": [
"document"
"document",
"ShadowRoot"
],
"node" : true,
"esnext": true,

View File

@ -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
});
}
}
}
}

View File

@ -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
];
}
}

View File

@ -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++;

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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);
});

View File

@ -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>');
};

View File

@ -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;

View File

@ -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._);

View File

@ -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();
};

View File

@ -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>

View File

@ -0,0 +1,3 @@
{
"<ce-test>": {}
}

View File

@ -0,0 +1 @@
<ce-test attr={ foo:'bar' }/>