Elliott Sprehn 9612dcf25f Don't set expression attributes before they're bound.
We were cloning elements with all the attributes that contained
expressions like attrName="{{ foo }}" which meant we'd go through the
reflection process converting that value and also call the
attrNameChanged() callback and the attributeChanged() with the braced
string which the element didn't really want to know about.

After this patch we create a "clone source node" which is a copy of the
original element without the attributes that have the expressions and
use that when cloning, then we assign the properties using data binding
later.

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

Review URL: https://codereview.chromium.org/868973002
2015-01-22 17:04:10 -08:00

196 lines
4.9 KiB
Plaintext

<!--
// 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" />
<import src="element-registry.sky" as="registry" />
<script>
function parseAttributeSpec(registration, definition) {
var spec = definition.getAttribute('attributes');
if (!spec)
return;
var attributeTokens = spec.split(',');
for (var i = 0; i < attributeTokens.length; ++i) {
var parts = attributeTokens[i].split(':');
if (parts.length != 2) {
console.error('Invalid attribute spec "' + spec + '", attributes must' +
' be {name}:{type}, where type is one of boolean, number or' +
' string.');
continue;
}
var name = parts[0].trim();
var type = parts[1].trim();
registration.defineAttribute(name, type);
}
}
function parseEventHandlers(registration, definition) {
var eventHandlers = [];
var attributes = definition.getAttributes();
for (var i = 0; i < attributes.length; i++) {
var attr = attributes[i];
var name = attr.name;
var value = attr.value;
if (name.startsWith('on-')) {
registration.eventHandlers.set(name.substring(3), value);
}
}
}
class SkyElement extends HTMLElement {
static register() {
var definition = document.currentScript.parentNode;
if (definition.localName !== 'sky-element') {
throw new Error('register() calls must be inside a <sky-element>.');
}
var tagName = definition.getAttribute('name');
if (!tagName) {
throw new Error('<sky-element> must have a name.');
}
var registration = registry.registerElement(tagName);
parseAttributeSpec(registration, definition);
parseEventHandlers(registration, definition);
registration.template = definition.querySelector('template');
registration.synthesizeAttributes(this.prototype);
return document.registerElement(tagName, {
prototype: this.prototype,
});
}
created() {
// override
}
attached() {
// override
}
detached() {
// override
}
attributeChanged(attrName, oldValue, newValue) {
// override
}
shadowRootReady() {
// override
}
createdCallback() {
this.isAttached = false;
this.propertyBindings = null;
this.dirtyPropertyBindings = null;
this.created();
Object.preventExtensions(this);
// Invoke attributeChanged callback when element is first created too.
var attributes = this.getAttributes();
for (var i = 0; i < attributes.length; ++i) {
var attribute = attributes[i];
this.attributeChangedCallback(attribute.name, null, attribute.value);
}
var registration = registry.getRegistration(this.localName);
registration.addInstanceEventListeners(this);
}
attachedCallback() {
if (!this.shadowRoot) {
var registration = registry.getRegistration(this.localName);
if (registration.template) {
var shadow = this.ensureShadowRoot();
var instance = binder.createInstance(registration.template, this);
shadow.appendChild(instance.fragment);
this.shadowRootReady();
}
}
this.attached();
this.isAttached = true;
}
detachedCallback() {
this.detached();
this.isAttached = false;
}
attributeChangedCallback(name, oldValue, newValue) {
if (registry.isExpandableAttribute(name))
return;
this.attributeChanged(name, oldValue, newValue);
var registration = registry.getRegistration(this.localName);
var converter = registration.attributes.get(name);
if (converter) {
this.notifyPropertyChanged(name, converter(oldValue),
converter(newValue));
}
}
notifyPropertyChanged(name, oldValue, newValue) {
if (oldValue == newValue)
return;
var notifier = Object.getNotifier(this);
notifier.notify({
type: 'update',
name: name,
oldValue: oldValue,
});
var handler = this[name + 'Changed'];
if (typeof handler == 'function')
handler.call(this, oldValue, newValue);
this.schedulePropertyBindingUpdate(name);
}
addPropertyBinding(name, binding) {
if (!this.propertyBindings)
this.propertyBindings = new Map();
this.propertyBindings.set(name, binding);
}
getPropertyBinding(name) {
if (!this.propertyBindings)
return null;
return this.propertyBindings.get(name);
}
schedulePropertyBindingUpdate(name) {
if (!this.dirtyPropertyBindings) {
this.dirtyPropertyBindings = new Set();
Promise.resolve().then(this.updatePropertyBindings.bind(this));
}
this.dirtyPropertyBindings.add(name);
}
updatePropertyBindings() {
for (var name of this.dirtyPropertyBindings) {
var binding = this.getPropertyBinding(name);
if (binding) {
binding.setValue(this[name]);
binding.discardChanges();
}
}
this.dirtyPropertyBindings = null;
}
};
module.exports = SkyElement;
</script>