diff --git a/src/ignite-element.js b/src/ignite-element.js index b7b949c..1cab669 100644 --- a/src/ignite-element.js +++ b/src/ignite-element.js @@ -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; } diff --git a/src/ignite-html.js b/src/ignite-html.js index 0a5d1d7..39dd564 100644 --- a/src/ignite-html.js +++ b/src/ignite-html.js @@ -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 }; \ No newline at end of file diff --git a/src/ignite-template.js b/src/ignite-template.js index 83756e7..a8634b7 100644 --- a/src/ignite-template.js +++ b/src/ignite-template.js @@ -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); } } diff --git a/src/main-app.js b/src/main-app.js index 476cc44..3fd25a4 100644 --- a/src/main-app.js +++ b/src/main-app.js @@ -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); } } diff --git a/src/sheet.js b/src/sheet.js index 7af86a8..9855f2e 100644 --- a/src/sheet.js +++ b/src/sheet.js @@ -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("