diff --git a/ignite-element.js b/ignite-element.js index 475f482..3350162 100644 --- a/ignite-element.js +++ b/ignite-element.js @@ -55,6 +55,7 @@ class IgniteElement extends HTMLElement { this.elements = []; this.createProperties(); this.readyCallback = new IgniteCallback(() => this.ready()); + this.onDisconnectCallbacks = []; //Init the element before connected callback is fired //Create a new rendering context so the init method can access properties correctly. @@ -249,11 +250,36 @@ class IgniteElement extends HTMLElement { //Detach the after render callback this.readyCallback.disconnect(); + //Call any on disconnected callbacks + if (this.onDisconnectCallbacks) { + this.onDisconnectCallbacks.forEach(callback => callback.invoke(this)); + } + //Cleanup this element if we need to. this.cleanup(); }, 1); } + /** + * Attaches a function to the on disconnect event for this element and returns a callback. + * @param {Function} onDisconnect Disconnect function to be called when on disconnect is raised. + * @returns IgniteCallback created for this callback. + */ + attachOnDisconnect(onDisconnect) { + var callback = new IgniteCallback(onDisconnect, detach => this.detachOnDisconnect(detach)); + this.onDisconnectCallbacks.push(callback); + return callback; + } + + /** + * Removes an ignite callback from the on disconnect event. + * @param {IgniteCallback} callback The ignite callback to disconnect. + * @ignore + */ + detachOnDisconnect(callback) { + this.onDisconnectCallbacks = this.onDisconnectCallbacks.filter(item => item != callback); + } + /** * Returns the template to be rendered for this element. * @@ -304,8 +330,8 @@ class IgniteElement extends HTMLElement { * Generates a uuid and returns it. */ uuid() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } } diff --git a/ignite-template.js b/ignite-template.js index 99097e5..7764c21 100644 --- a/ignite-template.js +++ b/ignite-template.js @@ -255,6 +255,7 @@ class IgniteTemplate { * (You can reflect a property more than once if it's needed.) * @param {String} name Name of the property to reflect. * @param {IgniteProperty|Function} target The target for the value to be reflected to. + * @returns This ignite template so function calls can be chained. */ reflect(name, target) { IgniteRenderingContext.push(); @@ -622,6 +623,7 @@ class IgniteTemplate { * Hides the element this template is constructing if the value is true. * @param {Boolean|IgniteProperty} value If true hides the element this template is constructing. If an IgniteProperty is passed it's value will auto update this. * @param {Function} converter An optional function to convert the value if needed. + * @returns This ignite template so function calls can be chained. */ hide(value, converter = null) { return this.style("display", value, true, (...params) => { @@ -633,6 +635,7 @@ class IgniteTemplate { * Shows the element this template is constructing if the value is true. * @param {Boolean|IgniteProperty} value * @param {Function} converter + * @returns This ignite template so function calls can be chained. */ show(value, converter = null) { return this.style("display", value, true, (...params) => { @@ -650,6 +653,16 @@ class IgniteTemplate { return this.attribute("id", value, converter); } + /** + * Sets the title attribute of the element to be constructed by this template. + * @param {String|IgniteProperty} value The value to set for the title attribute of the element this template will construct. + * @param {Function} converter An optional function that can convert the value if needed. + * @returns This ignite template so function calls can be chained. + */ + title(value, converter = null) { + return this.attribute("title", value, converter); + } + /** * Sets the for attribute of the element to be constructed by this template. * @param {String|IgniteProperty} value The value to set for the for attribute of the element this template will construct. @@ -1650,23 +1663,26 @@ class list extends IgniteTemplate { /** * @param {Array|IgniteProperty} list The list of items to construct within this template. * @param {Function} forEach A function that construct a template for an item from the list that is passed to it. + * @param {Boolean} reflect If true any items removed from the DOM will be removed from the list if they exist. By default this is false. */ - constructor(list, forEach) { + constructor(list, forEach, reflect = false) { super(); if (list instanceof IgniteProperty) { - this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(oldValue, newValue))); - this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(list, items))); - this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(list, items))); - this._callbacks.push(list.attachOnPop(list => this.onListPop(list))); - this._callbacks.push(list.attachOnShift(list => this.onListShift(list))); - this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(list, start, deleteCount, items))); + this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(newValue))); + this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(items))); + this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(items))); + this._callbacks.push(list.attachOnPop(list => this.onListPop())); + this._callbacks.push(list.attachOnShift(list => this.onListShift())); + this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(start, deleteCount, items))); this.list = list.value; } else { this.list = list; } + this.reflecting = reflect; + this.reflectCallbacks = []; this.forEach = forEach; this.elements = []; this.tagName = "shadow list"; @@ -1690,6 +1706,15 @@ class list extends IgniteTemplate { parent = this.element.parentElement; } + //If we have any reflect callbacks, remove them. + //(If we dont do this, we will accidentally remove items from the list potentially) + if (this.reflectCallbacks.length > 0) { + for (var i = 0; i < this.reflectCallbacks.length; i++) { + this.reflectCallbacks[i].disconnect(); + } + this.reflectCallbacks = []; + } + //If we already have elements we created, destroy them. if (this.elements.length > 0) { for (var i = 0; i < this.elements.length; i++) { @@ -1713,13 +1738,18 @@ class list extends IgniteTemplate { var template = this.forEach(this.list[i]); template.construct(parent, this.element); + //If we are reflecting, attach to the elements disconnect event. + if (this.reflecting) { + this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(this.list[i]))); + } + this.children.push(template); this.elements.push(template.element); } } } - onListChanged(oldValue, newValue) { + onListChanged(newValue) { this.list = newValue; IgniteRenderingContext.enter(); @@ -1733,7 +1763,7 @@ class list extends IgniteTemplate { IgniteRenderingContext.leave(); } - onListPush(list, items) { + onListPush(items) { IgniteRenderingContext.enter(); try { @@ -1746,6 +1776,11 @@ class list extends IgniteTemplate { template.construct(this.element.parentElement, this.element); } + //If we are reflecting, attach to the elements disconnect event. + if (this.reflecting) { + this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item))); + } + this.children.push(template); this.elements.push(template.element); }); @@ -1756,7 +1791,7 @@ class list extends IgniteTemplate { IgniteRenderingContext.leave(); } - onListUnshift(list, items) { + onListUnshift(items) { IgniteRenderingContext.enter(); try { @@ -1770,6 +1805,10 @@ class list extends IgniteTemplate { template.construct(this.element.parentElement, this.element); } + if (this.reflecting) { + this.reflectCallbacks.unshift(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item))); + } + this.children.unshift(template); this.elements.unshift(template.element); }); @@ -1780,33 +1819,51 @@ class list extends IgniteTemplate { IgniteRenderingContext.leave(); } - onListPop(list) { + onListPop() { if (this.children.length > 0) { this.children[this.children.length - 1].deconstruct(); this.children.pop(); this.elements.pop(); + + if (this.reflecting) { + this.reflectCallbacks[this.reflectCallbacks.length - 1].disconnect(); + this.reflectCallbacks.pop(); + } } } - onListShift(list) { + onListShift() { if (this.children.length > 0) { this.children[0].deconstruct(); this.children.shift(); this.elements.shift(); + + if (this.reflecting) { + this.reflectCallbacks[0].disconnect(); + this.reflectCallbacks.shift(); + } } } - onListSplice(list, start, deleteCount, items) { + onListSplice(start, deleteCount, items) { IgniteRenderingContext.enter(); - //Remove any items that are needed. + //Remove any items that will no longer exist. if (deleteCount > 0 && this.children.length > 0) { for (var i = start; i < Math.min(this.children.length, start + deleteCount); i++) { this.children[i].deconstruct(); + + if (this.reflecting) { + this.reflectCallbacks[i].disconnect(); + } } this.children.splice(start, deleteCount); this.elements.splice(start, deleteCount); + + if (this.reflecting) { + this.reflectCallbacks.splice(start, deleteCount); + } } //If the start is greater than the length of the items adjust it. @@ -1825,6 +1882,10 @@ class list extends IgniteTemplate { template.construct(this.element.parentElement, this.element); } + if (this.reflecting) { + this.reflectCallbacks.splice(start, 0, template.element.attachOnDisconnect(disconnect => this.onItemRemove(item))); + } + this.children.splice(start, 0, template); this.elements.splice(start, 0, template.element); @@ -1835,6 +1896,14 @@ class list extends IgniteTemplate { IgniteRenderingContext.leave(); } + onItemRemove(item) { + var index = this.list.indexOf(item); + + if (index >= 0) { + this.list.splice(index, 1); + } + } + onStyleChanged(name, newValue) { this.elements.forEach((element) => { element.style.setProperty(name, newValue, this._styles[name].priority);