Combine element registries from sky-binder and sky-element.

We now have an element-registry.sky module that exposes API to register
elements with attributes and event handlers. Right now it's an API on
the module itself, eventually the module will export an ElementRegistry
object since we'll need one per module once the custom element registry
is one per module.

R=abarth@chromium.org, ojan@chromium.org

Review URL: https://codereview.chromium.org/856693002
This commit is contained in:
Elliott Sprehn 2015-01-15 20:31:32 -08:00
parent 554ccd0de1
commit 8291177a0c
3 changed files with 177 additions and 149 deletions

View File

@ -0,0 +1,148 @@
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="sky-binder.sky" as="binder" />
<script>
// TODO(esprehn): It would be nice if these were exposed by the platform so
// the framework didn't need to hard code a list.
var defaultAttributesNames = new Set([
'accesskey',
'alt',
'as',
'async',
'class',
'contenteditable',
'crossorigin',
'dir',
'height',
'href',
'id',
'is',
'lang',
'media',
'name',
'rel',
'select',
'sizes',
'spellcheck',
'src',
'srcset',
'style',
'tabindex',
'title',
'type',
'width',
]);
var attributeConverters = {
boolean: function(value) {
if (typeof value == 'string')
return value == 'true';
return !!value;
},
number: function(value) {
return Number(value);
},
string: function(value) {
if (value === null)
return '';
return String(value);
},
};
function eventHandlerCallback(event) {
var element = event.currentTarget;
var registration = getRegistration(element.localName);
var method = registration.eventHandlers.get(event.type);
var handler = element[method];
if (!(typeof handler == 'function')) {
throw new Error('Element ' + element.localName +
' specifies invalid event handler "' + method + '"');
}
handler.call(element, event);
}
class ElementRegistration {
constructor(tagName) {
this.tagName = tagName;
this.attributes = new Map();
this.eventHandlers = new Map();
this.template = null;
Object.preventExtensions(this);
}
allowsAttribute(name) {
if (name.startsWith('data-'))
return true;
if (defaultAttributesNames.has(name))
return true;
if (this.attributes.has(name))
return true;
return false;
}
defineAttribute(name, type) {
var converter = attributeConverters[type];
if (!converter) {
console.error('Invalid attribute type "' + type + '", type must be one'
+ ' of boolean, number or string.');
return;
}
this.attributes.set(name, converter);
}
synthesizeAttributes(prototype) {
this.attributes.forEach(function(converter, name) {
Object.defineProperty(prototype, name, {
get: function() {
return converter(this.getAttribute(name));
},
set: function(newValue) {
this.setAttribute(name, converter(newValue));
},
enumerable: true,
configurable: true,
});
});
}
addInstanceEventListeners(instance) {
for (var eventName of this.eventHandlers.keys()) {
instance.addEventListener(eventName, eventHandlerCallback);
}
}
}
var registrations = new Map();
function registerElement(tagName) {
if (registrations.has(tagName))
throw new Error('tagName "' + tagName + '" registered twice.');
var registration = new ElementRegistration(tagName);
registrations.set(tagName, registration);
return registration;
}
function getRegistration(tagName) {
return registrations.get(tagName);
}
function checkAttribute(tagName, attrName) {
var registration = getRegistration(tagName);
if (!registration)
return defaultAttributesNames.has(attrName);
return registration.allowsAttribute(attrName);
}
module.exports = {
registerElement: registerElement,
getRegistration: getRegistration,
checkAttribute: checkAttribute,
};
</script>

View File

@ -4,6 +4,7 @@
// found in the LICENSE file.
-->
<import src="observe.sky" as="observe" />
<import src="element-registry.sky" as="registry" />
<script>
var stagingDocument = new Document();
@ -26,45 +27,11 @@ class TemplateInstance {
var emptyInstance = new TemplateInstance();
var directiveCache = new WeakMap();
// TODO(esprehn): It would be nice if these were exposed by the platform so
// the framework didn't need to hard code a list.
var defaultAttributesNames = new Set([
'accesskey',
'alt',
'as',
'async',
'class',
'contenteditable',
'crossorigin',
'dir',
'height',
'href',
'id',
'is',
'lang',
'media',
'name',
'rel',
'select',
'sizes',
'spellcheck',
'src',
'srcset',
'style',
'tabindex',
'title',
'type',
'width',
]);
var elementAttributeNames = new Map();
function registerElement(name, options) {
elementAttributeNames.set(name, new Set(options.attributeNames));
}
registerElement('template', {
attributeNames: ['if', 'repeat'],
});
(function() {
var templateRegistration = registry.registerElement('template');
templateRegistration.defineAttribute('if', 'string');
templateRegistration.defineAttribute('repeat', 'string');
})();
function createInstance(template, model) {
var content = template.content;
@ -196,20 +163,9 @@ function parsePropertyDirective(value, property) {
return result;
}
function checkAttribute(name, allowedAttributeNames) {
if (name.startsWith('data-'))
return true;
if (defaultAttributesNames.has(name))
return true;
if (allowedAttributeNames && allowedAttributeNames.has(name))
return true;
return false;
}
function parseAttributeDirectives(element, directives) {
var attributes = element.getAttributes();
var allowedAttributeNames = elementAttributeNames.get(element.tagName);
var tagName = element.tagName;
for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i];
@ -221,8 +177,8 @@ function parseAttributeDirectives(element, directives) {
continue;
}
if (!checkAttribute(name, allowedAttributeNames)) {
console.error('Element "'+ element.tagName +
if (!registry.checkAttribute(tagName, name)) {
console.error('Element "'+ tagName +
'" has unknown attribute "' + name + '".');
}
@ -526,6 +482,5 @@ class TemplateIterator {
module.exports = {
createInstance: createInstance,
registerElement: registerElement,
};
</script>

View File

@ -4,28 +4,13 @@
// found in the LICENSE file.
-->
<import src="sky-binder.sky" as="binder" />
<import src="element-registry.sky" as="registry" />
<script>
var attributeConverters = {
boolean: function(value) {
if (typeof value == 'string')
return value == 'true';
return !!value;
},
number: function(value) {
return Number(value);
},
string: function(value) {
if (value === null)
return '';
return String(value);
},
};
function parseAttributeSpec(spec) {
var attributes = new Map();
function parseAttributeSpec(registration, definition) {
var spec = definition.getAttribute('attributes');
if (!spec)
return attributes;
return;
var attributeTokens = spec.split(',');
@ -41,21 +26,12 @@ function parseAttributeSpec(spec) {
var name = parts[0].trim();
var type = parts[1].trim();
var converter = attributeConverters[type];
if (!converter) {
console.error('Invalid attribute spec "' + spec + '", type must be one'
+ ' of boolean, number or string.');
continue;
}
attributes.set(name, converter);
registration.defineAttribute(name, type);
}
return attributes;
}
function collectEventHandlers(definition) {
function parseEventHandlers(registration, definition) {
var eventHandlers = [];
var attributes = definition.getAttributes();
@ -65,54 +41,11 @@ function collectEventHandlers(definition) {
var value = attr.value;
if (name.startsWith('on-')) {
eventHandlers.push(name.substring(3));
registration.eventHandlers.set(name.substring(3), value);
}
}
return eventHandlers;
}
function eventHandlerCallback(event) {
var element = event.currentTarget;
var registration = registrations.get(element.localName);
var method = registration.getEventHandler(event.type);
var handler = element[method];
if (handler instanceof Function)
return handler.call(element, event);
}
class ElementRegistration {
constructor(definition) {
this.definition = definition;
this.tagName = definition.getAttribute('name');
this.attributes = parseAttributeSpec(definition.getAttribute('attributes'));
this.eventHandlers = collectEventHandlers(definition);
this.template = definition.querySelector('template');
Object.preventExtensions(this);
}
getEventHandler(eventName) {
return this.definition.getAttribute('on-' + eventName);
}
synthesizeAttributes(prototype) {
this.attributes.forEach(function(converter, name) {
Object.defineProperty(prototype, name, {
get: function() {
return converter(this.getAttribute(name));
},
set: function(newValue) {
this.setAttribute(name, converter(newValue));
},
enumerable: true,
configurable: true,
});
});
}
}
var registrations = new Map();
class SkyElement extends HTMLElement {
static register() {
@ -122,26 +55,21 @@ class SkyElement extends HTMLElement {
throw new Error('register() calls must be inside a <sky-element>.');
}
var registration = new ElementRegistration(definition);
if (!registration.tagName) {
var tagName = definition.getAttribute('name');
if (!tagName) {
throw new Error('<sky-element> must have a name.');
}
if (registrations.has(registration.tagName)) {
throw new Error('Duplicate registration for tag name: ' +
registration.tagName);
}
var registration = registry.registerElement(tagName);
parseAttributeSpec(registration, definition);
parseEventHandlers(registration, definition);
registration.template = definition.querySelector('template');
registration.synthesizeAttributes(this.prototype);
// TODO(esprehn): Combine the two element registries here and in sky binder.
binder.registerElement(registration.tagName, {
attributeNames: Array.from(registration.attributes.keys()),
});
registrations.set(registration.tagName, registration);
return document.registerElement(registration.tagName, {
return document.registerElement(tagName, {
prototype: this.prototype,
});
}
@ -181,16 +109,13 @@ class SkyElement extends HTMLElement {
this.attributeChangedCallback(attribute.name, null, attribute.value);
}
var registration = registrations.get(this.localName);
for (var i = 0; i < registration.eventHandlers.length; ++i) {
var eventName = registration.eventHandlers[i];
this.addEventListener(eventName, eventHandlerCallback);
}
var registration = registry.getRegistration(this.localName);
registration.addInstanceEventListeners(this);
}
attachedCallback() {
if (!this.shadowRoot) {
var registration = registrations.get(this.localName);
var registration = registry.getRegistration(this.localName);
if (registration.template) {
var shadow = this.ensureShadowRoot();
var instance = binder.createInstance(registration.template, this);
@ -209,7 +134,7 @@ class SkyElement extends HTMLElement {
attributeChangedCallback(name, oldValue, newValue) {
this.attributeChanged(name, oldValue, newValue);
var registration = registrations.get(this.localName);
var registration = registry.getRegistration(this.localName);
var converter = registration.attributes.get(name);
if (converter) {
this.notifyPropertyChanged(name, converter(oldValue),