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:
		| @@ -5,16 +5,24 @@ class IgniteElement extends HTMLElement { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         this.onDisconnected = null; | ||||
|         this.template = null; | ||||
|         this.createProperties(); | ||||
|     } | ||||
|  | ||||
|     properties() { | ||||
|     /** | ||||
|      * Returns the properties for this ignite element. | ||||
|      */ | ||||
|     get properties() { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates the getters/setters for properties in this | ||||
|      * ignite element and initializes everything. | ||||
|      */ | ||||
|     createProperties() { | ||||
|         var props = this.properties(); | ||||
|         var props = this.properties; | ||||
|  | ||||
|         for (var i = 0; i < props.length; i++) { | ||||
|             this[`_${props[i]}`] = new IgniteProperty(); | ||||
| @@ -33,13 +41,47 @@ class IgniteElement extends HTMLElement { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Setups this ignite element and constructs it's template when | ||||
|      * this function is called by the DOM upon this element being created somewhere. | ||||
|      */ | ||||
|     connectedCallback() { | ||||
|         this.template = new IgniteTemplate(); | ||||
|         this.template.element = this; | ||||
|         this.template.tagName = this.tagName; | ||||
|  | ||||
|         this.render().construct(this); | ||||
|         //Make sure the render template is our template, if not, add it as a child. | ||||
|         var renderTemplate = this.render(); | ||||
|         if (renderTemplate !== this.template && renderTemplate) { | ||||
|             this.template.child(renderTemplate); | ||||
|         } else if (!renderTemplate) { | ||||
|             console.warn(`RenderTemplate was null for element: ${this.tagName}, is render() returning null or not returning anything?`); | ||||
|         } | ||||
|  | ||||
|         //Construct our template. | ||||
|         this.template.construct(this.parentElement); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Cleanups this element and deconstructs everything when this element is removed from | ||||
|      * the DOM. | ||||
|      */ | ||||
|     disconnectedCallback() { | ||||
|         //If we still have a reference to our template, deconstruct it. | ||||
|         if (this.template) { | ||||
|             this.template.deconstruct(); | ||||
|         } | ||||
|  | ||||
|         //If we have a onDisconnected callback, call it and then remove the reference. | ||||
|         if (this.onDisconnected) { | ||||
|             this.onDisconnected(); | ||||
|             this.onDisconnected = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the template to be rendered for this element. | ||||
|      */ | ||||
|     render() { | ||||
|         return null; | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| class IgniteProperty { | ||||
|     constructor() { | ||||
|         this.onPropertyChange = []; | ||||
|         this.callbacks = []; | ||||
|         this._value = null; | ||||
|     } | ||||
|  | ||||
| @@ -12,17 +12,41 @@ class IgniteProperty { | ||||
|         var old = this._value; | ||||
|         this._value = val; | ||||
|  | ||||
|         for (var i = 0; i < this.onPropertyChange.length; i++) { | ||||
|             this.onPropertyChange[i](old, val); | ||||
|         //Invoke any callbacks letting them know the value changed. | ||||
|         for (var i = 0; i < this.callbacks.length; i++) { | ||||
|             this.callbacks[i].invoke(old, val); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     attach(onChange) { | ||||
|         var callback = new IgnitePropertyCallback(this, onChange); | ||||
|         this.callbacks.push(callback); | ||||
|         return callback; | ||||
|     } | ||||
| } | ||||
|  | ||||
| class IgnitePropertyCallback { | ||||
|     constructor(property, callback) { | ||||
|         this.callback = callback; | ||||
|         this.property = property; | ||||
|     } | ||||
|  | ||||
|     invoke(oldValue, newValue) { | ||||
|         if (this.callback) { | ||||
|             this.callback(oldValue, newValue); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     disconnect() { | ||||
|         this.callback = null; | ||||
|  | ||||
|         if (this.property) { | ||||
|             this.property.callbacks = this.property.callbacks.filter(callback => callback != this); | ||||
|             this.property = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| class IgniteAttribute { | ||||
|  | ||||
| } | ||||
|  | ||||
| export { | ||||
|     IgniteProperty, | ||||
|     IgniteAttribute | ||||
|     IgniteProperty | ||||
| }; | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,22 +6,17 @@ class MainApp extends IgniteElement { | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         this.title = "Default Title"; | ||||
|         this.name = "Default Name"; | ||||
|         this.href = "www.overrided.com"; | ||||
|     } | ||||
|  | ||||
|     properties() { | ||||
|     get properties() { | ||||
|         return [ | ||||
|             "title", | ||||
|             "name", | ||||
|             "href", | ||||
|             "sheet" | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         return new Sheet().property("name", this.name).property("href", this.href).ref(this.sheet); | ||||
|         return new Sheet().property("name", this.name); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,9 +8,10 @@ class Sheet extends IgniteElement { | ||||
|         this.show = false; | ||||
|         this.items = []; | ||||
|         this.href = "www.google.com"; | ||||
|         this.name = "default content"; | ||||
|     } | ||||
|  | ||||
|     properties() { | ||||
|     get properties() { | ||||
|         return [ | ||||
|             "show", | ||||
|             "name", | ||||
| @@ -26,10 +27,9 @@ class Sheet extends IgniteElement { | ||||
|                 new div( | ||||
|                     new html("<h2>this is before</h2>"), | ||||
|                     new list(this.items, (item) => { | ||||
|                         return new a(`${item}`).attribute("href", this.href) | ||||
|                         return new a(this.name).attribute("href", this.href) | ||||
|                     }), | ||||
|                     new html("<h2>this is after</h2>"), | ||||
|                     new a("this is a link").attribute("href", this.href) | ||||
|                     new html("<h2>this is after</h2>") | ||||
|                 ) | ||||
|             ); | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user