Added deconstruct functionality and cleanup code so that everything gets correctly cleaned up once a element is removed from the DOM. Added a new property template that allows properties to be used as content or html anywhere within a template's children. Next up is slot support.

This commit is contained in:
2020-07-28 22:23:49 -07:00
parent 1adb844c97
commit 43962757f0
5 changed files with 255 additions and 54 deletions

View File

@ -1,4 +1,4 @@
import { IgniteProperty, IgniteAttribute } from './ignite-html.js';
import { IgniteProperty } from './ignite-html.js';
class IgniteTemplate {
constructor(items) {
@ -10,13 +10,12 @@ class IgniteTemplate {
this.element = null;
this.properties = {};
this.refs = [];
this.callbacks = [];
if (items) {
for (var i = 0; i < items.length; i++) {
if (items[i] instanceof IgniteAttribute) {
this.attributes.push(items[i]);
} else if (items[i] instanceof IgniteProperty) {
this.children.push(new propertyObserver(items[i]));
if (items[i] instanceof IgniteProperty) {
this.children.push(new property(items[i]));
} else {
this.children.push(items[i]);
}
@ -24,10 +23,15 @@ class IgniteTemplate {
}
}
/**
* Adds a single or series of classes to this template
* to be added once this template is constructed.
* @param {...any} items
*/
class(...items) {
for (var i = 0; i < items.length; i++) {
if (items[i] instanceof IgniteProperty) {
items[i].onPropertyChange.push((oldValue, newValue) => this.onClassChanged(oldValue, newValue));
this.callbacks.push(items[i].attach(this.onClassChanged));
this.classes.push(items[i].value);
} else {
this.classes.push(items[i]);
@ -37,9 +41,14 @@ class IgniteTemplate {
return this;
}
/**
* Adds a attribute to this template to be added once this template is constructed.
* @param {string} name
* @param {any} value
*/
attribute(name, value) {
if (value instanceof IgniteProperty) {
value.onPropertyChange.push((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name));
this.callbacks.push(value.attach((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name)));
this.attributes[name] = value.value;
} else {
this.attributes[name] = value;
@ -48,9 +57,14 @@ class IgniteTemplate {
return this;
}
/**
* Adds a property to this template to be added once this template is constructed.
* @param {string} name
* @param {any} value
*/
property(name, value) {
if (value instanceof IgniteProperty) {
value.onPropertyChange.push((oldValue, newValue) => this.onPropertyChanged(oldValue, newValue, name));
this.callbacks.push(value.attach((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name)));
this.properties[name] = value.value;
} else {
this.properties[name] = value;
@ -59,12 +73,27 @@ class IgniteTemplate {
return this;
}
/**
* Adds a single or series of children to be added once this template
* is constructed.
* @param {...any} items
*/
child(...items) {
this.children = this.children.concat(items);
for (var i = 0; i < items.length; i++) {
if (items[i] instanceof IgniteProperty) {
this.children.push(new property(items[i]));
} else {
this.children.push(items[i]);
}
}
return this;
}
/**
* Adds a ref callback function to be invoked once this template is constructed.
* @param {function} item
*/
ref(item) {
if (item instanceof IgniteProperty) {
this.refs.push((element) => {
@ -77,11 +106,28 @@ class IgniteTemplate {
return this;
}
construct(parent) {
/**
* Constructs this template and adds it to the DOM if this template
* has not already been constructed.
* @param {HTMLElement} parent
* @param {HTMLElement} sibling
*/
construct(parent, sibling) {
//Construct this element if we haven't already
if (!this.element) {
this.element = window.document.createElement(this.tagName);
parent.appendChild(this.element);
if (sibling) {
parent.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
//If the element has a onDisconnected function, attach to it
//(This way if a custom element is removed we can deconstruct and cleanup)
if (this.element.onDisconnected !== undefined) {
this.element.onDisconnected = () => this.deconstruct();
}
//Invoke any refs we have
this.refs.forEach((ref) => { ref(this.element); });
@ -118,9 +164,48 @@ class IgniteTemplate {
}
}
/**
* Deconstructs this template and cleans up all resources to make sure
* there are no memory leaks.
*/
deconstruct() {
console.log(`Deconstructing ${this.tagName}`);
//Remove our element if we have one.
if (this.element) {
this.element.remove();
this.element = null;
}
//Deconstruct all children elements.
if (this.children) {
for (var i = 0; i < this.children.length; i++) {
if (this.children[i] instanceof IgniteTemplate || this.children[i].prototype instanceof IgniteTemplate) {
this.children[i].deconstruct();
}
}
this.children = null;
}
//disconnect all callbacks
if (this.callbacks) {
this.callbacks.forEach((item) => item.disconnect());
this.callbacks = null;
}
//remove any refs
if (this.refs) {
this.refs = null;
}
}
/**
* Called when a class on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
*/
onClassChanged(oldValue, newValue) {
console.log(`Class changed, oldValue: ${oldValue} newValue: ${newValue}`);
if (oldValue !== null && oldValue !== undefined && oldValue !== "" && oldValue !== " ") {
this.element.classList.remove(oldValue);
}
@ -130,11 +215,15 @@ class IgniteTemplate {
}
}
/**
* Called when a attribute on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {string} attributeName
*/
onAttributeChanged(oldValue, newValue, attributeName) {
console.log(`Attribute changed, oldValue: ${oldValue} newValue: ${newValue} attribute: ${attributeName}`);
console.log("this element:");
console.log(this.element);
if (newValue == null || newValue == undefined) {
this.element.removeAttribute(attributeName);
} else {
@ -142,6 +231,13 @@ class IgniteTemplate {
}
}
/**
* Called when a property on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {string} propertyName
*/
onPropertyChanged(oldValue, newValue, propertyName) {
console.log(`Property changed, oldValue: ${oldValue} newValue: ${newValue} property: ${propertyName}`);
this.properties[propertyName] = newValue;
@ -169,6 +265,7 @@ class html extends IgniteTemplate {
constructor(code) {
super([]);
this.code = code;
this.tagName = "shadow html";
}
construct(parent) {
@ -183,12 +280,13 @@ class html extends IgniteTemplate {
class list extends IgniteTemplate {
constructor(list, forEach) {
super([]);
this.tagName = "shadow list";
if (list instanceof IgniteProperty) {
list.onPropertyChange.push((oldValue, newValue) => {
this.list = list.value;
this.construct(null); //If the list changed, reconstruct this template
});
this.callbacks.push(list.attach((oldValue, newValue) => {
this.list = newValue;
this.construct(null); //If the list changed, reconstruct this template.
}));
this.list = list.value;
} else {
@ -199,10 +297,12 @@ class list extends IgniteTemplate {
this.elements = [];
}
construct(parent) {
construct(parent, sibling) {
if (!this.element) {
this.element = window.document.createTextNode(""); //Use a textnode as our placeholder
parent.appendChild(this.element);
} else {
parent = this.element.parentElement;
}
//If we already have elements we created, destroy them.
@ -213,25 +313,65 @@ class list extends IgniteTemplate {
this.elements = [];
}
//Create a temporary container
var container = window.document.createElement("div");
//If we already have children elements, deconstruct them.
//(Because we are about to recreate them)
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].deconstruct();
}
this.children = [];
}
//Construct all the items in our list and use the container
if (this.list) {
for (var i = 0; i < this.list.length; i++) {
this.forEach(this.list[i]).construct(container);
var template = this.forEach(this.list[i]);
template.construct(parent, this.element);
this.children.push(template);
this.elements.push(template.element);
}
}
}
}
class property extends IgniteTemplate {
constructor(prop) {
super(null);
this.property = prop;
this.elements = [];
this.callbacks.push(this.property.attach((oldValue, newValue) => this.onPropertyChanged(oldValue, newValue)));
}
construct(parent, sibling) {
if (!this.element) {
this.element = window.document.createTextNode("");
if (sibling) {
parent.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
}
//If we have any child nodes in the container, save the element, and add it after our placeholder
while (container.childNodes.length > 0) {
this.elements.push(container.childNodes[0]);
this.element.parentElement.insertBefore(container.childNodes[0], this.element);
}
//Remove any elements that already exist.
this.elements.forEach((item) => item.remove());
//Make sure we cleanup the container to be safe.
container.remove();
container = null;
//Create a template to hold the elements that will be created from the
//properties value and then add them to the DOM and store their pointer.
var template = window.document.createElement("template");
template.innerHTML = this.property.value;
while (template.content.childNodes.length > 0) {
var item = template.content.childNodes[0];
this.element.parentElement.insertBefore(item, this.element);
this.elements.push(item);
}
template.remove();
}
onPropertyChanged(oldValue, newValue) {
this.construct(null, null);
}
}