Many updates and improvements. For styles you can now combine two properties with a converter to get more custom styling updates. Cleaned up code and made a few things easier to use, also fixed a few bugs.

This commit is contained in:
Matt Mo 2020-09-25 09:36:39 -07:00
parent 0fcb908941
commit 2d01b8fafb
3 changed files with 315 additions and 51 deletions

View File

@ -115,24 +115,32 @@ class IgniteElement extends HTMLElement {
if (props != null) {
var keys = Object.keys(props);
for (var i = 0; i < keys.length; i++) {
let prop = new IgniteProperty(props[keys[i]]);
this[`_${keys[i]}`] = prop;
let propValue = props[keys[i]];
let propName = keys[i];
((propName) => {
Object.defineProperty(this, propName, {
get: function () {
if (IgniteRenderingContext.rendering == false) {
return this[`_${propName}`].value;
} else {
return this[`_${propName}`];
}
},
//Create a new property, if the propValue is a property, use that instead.
var prop = null;
if (propValue instanceof IgniteProperty) {
prop = propValue;
} else {
prop = new IgniteProperty(propValue);
}
set: function (value) {
this[`_${propName}`].value = value;
this[`_${propName}`] = prop;
Object.defineProperty(this, propName, {
get: () => {
if (IgniteRenderingContext.rendering == false) {
return this[`_${propName}`].value;
} else {
return this[`_${propName}`];
}
});
})(keys[i]);
},
set: (value) => {
this[`_${propName}`].value = value;
}
});
}
}
}

View File

@ -4,7 +4,7 @@
* @ignore
*/
class IgniteProperty {
constructor(val) {
constructor(val, onChange = null) {
this.onChangeCallbacks = [];
this.onPushCallbacks = [];
this.onPopCallbacks = [];
@ -13,6 +13,11 @@ class IgniteProperty {
this._value = val;
this.ignoreValueChange = false;
//If we were passed an onchange function attach it.
if (onChange) {
this.attachOnChange(onChange);
}
//Attempt to patch the value if it's a list.
this.patchArray();
}
@ -40,6 +45,7 @@ class IgniteProperty {
//Invoke any callbacks letting them know the value changed.
this.invokeOnChange(old, val);
//If we want to reflect the value then bubble it up.
if (reflect) {
this.ignoreValueChange = true; //Ignore changes incase we are connected to any reflected properties.
this.reflected.forEach(reflect => reflect.value = val);
@ -157,6 +163,30 @@ IgniteProperty.prototype.toString = function () {
return this.value.toString();
}
/**
* Add a prototype to help get property values from an array
*/
Array.prototype.getPropertyValues = function () {
var ret = [];
this.forEach(prop => ret.push(prop.value));
return ret;
}
/**
* Add a prototype to help get old property values from an array
*/
Array.prototype.getOldPropertyValues = function (property, oldValue) {
var ret = [];
this.forEach(prop => {
if (prop == property) {
ret.push(oldValue);
} else {
ret.push(prop.value);
}
});
return ret;
}
/**
* The outline of a ignite callback that can be invoked and disconnected
* to help maintain and cleanup callbacks.
@ -205,6 +235,20 @@ class IgniteRenderingContext {
}
}
static push() {
if (IgniteRenderingContext.Stack == null) {
IgniteRenderingContext.Stack = [];
}
IgniteRenderingContext.Stack.push(IgniteRenderingContext.RenderCount);
IgniteRenderingContext.RenderCount = 0;
}
static pop() {
if (IgniteRenderingContext.Stack && IgniteRenderingContext.Stack.length > 0) {
IgniteRenderingContext.RenderCount = IgniteRenderingContext.Stack.pop();
}
}
static ready(callback) {
//Setup the callbacks if it's not init'd.
if (!IgniteRenderingContext.ReadyCallbacks) {

View File

@ -35,6 +35,7 @@ class IgniteTemplate {
this.events = {};
this.styles = {};
this.elementValue = null;
this.elementInnerHTML = null;
if (children) {
for (var i = 0; i < children.length; i++) {
@ -62,6 +63,8 @@ class IgniteTemplate {
* @returns This ignite template so function calls can be chained.
*/
class(name, converter = null) {
IgniteRenderingContext.push();
if (name instanceof IgniteProperty) {
this.callbacks.push(name.attachOnChange((oldValue, newValue) => this.onClassChanged(oldValue, newValue, converter)));
var value = (converter != null ? converter(name.value) : name.value);
@ -81,6 +84,7 @@ class IgniteTemplate {
});
}
IgniteRenderingContext.pop();
return this;
}
@ -92,6 +96,8 @@ class IgniteTemplate {
* @returns This ignite template so function calls can be chained.
*/
attribute(name, value, converter = null) {
IgniteRenderingContext.push();
if (value instanceof IgniteProperty) {
this.callbacks.push(value.attachOnChange((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name, converter)));
this.attributes[name] = converter != null ? converter(value.value) : value.value;
@ -99,6 +105,7 @@ class IgniteTemplate {
this.attributes[name] = converter != null ? converter(value) : value;
}
IgniteRenderingContext.pop();
return this;
}
@ -111,6 +118,8 @@ class IgniteTemplate {
* @returns This ignite template so function calls can be chained.
*/
value(value, reflect = false, converter = null) {
IgniteRenderingContext.push();
if (reflect && converter != null) {
throw `Cannot set a value on an IgniteTemplate: ${this.tagName} with reflect and a converter used at the same time.`;
}
@ -135,6 +144,7 @@ class IgniteTemplate {
this.elementValue = (converter != null ? converter(value) : value);
}
IgniteRenderingContext.pop();
return this;
}
@ -147,6 +157,8 @@ class IgniteTemplate {
* @returns This ignite template so function calls can be chained.
*/
property(name, value, reflect = false, converter = null) {
IgniteRenderingContext.push();
if (this.properties[name]) {
throw `Attempted to set a property twice on a IgniteTemplate: ${this.tagName}. This is not allowed and should be avoided.`;
}
@ -168,6 +180,27 @@ class IgniteTemplate {
};
}
IgniteRenderingContext.pop();
return this;
}
/**
* Sets the inner html of the element to be constructed by this template.
* @param {String|IgniteProperty} value InnerHTML to set for element. If a property is passed the html will auto update.
* @param {Function} converter Optional function that can be used to convert the value if needed.
* @returns This ignite template so funciton calls can be chained.
*/
innerHTML(value, converter = null) {
IgniteRenderingContext.push();
if (value instanceof IgniteProperty) {
this.callbacks.push(value.attachOnChange((oldValue, newValue) => this.onInnerHTMLChanged(oldValue, newValue, converter)));
this.elementInnerHTML = (converter != null ? converter(value.value) : value.value);
} else {
this.elementInnerHTML = (converter != null ? converter(value) : value);
}
IgniteRenderingContext.pop();
return this;
}
@ -243,9 +276,25 @@ class IgniteTemplate {
* @returns This ignite template so function calls can be chained.
*/
onClick(eventCallback) {
this.on("click", eventCallback);
return this.on("click", eventCallback);
}
return this;
/**
* Adds a onblur event handler to this template.
* @param {Function|IgniteProperty} eventCallback THe callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onBlur(eventCallback) {
return this.on("blur", eventCallback);
}
/**
* Adds a onfocus event handler to this template.
* @param {Function|IgniteProperty} eventCallback THe callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onFocus(eventCallback) {
return this.on("focus", eventCallback);
}
/**
@ -269,12 +318,8 @@ class IgniteTemplate {
}
};
//Store the wrapped function so that it's the old value next time around
//and the old event can be removed in the future
eventCallback._value = wrapped;
//Invoke the on event changed with the old value and our wrapped value.
this.onEventChanged(oldValue, wrapped, eventName)
eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
}));
//Create the initial wrapper
@ -300,25 +345,121 @@ class IgniteTemplate {
return this;
}
/**
* Adds a on backspace key press event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onBackspace(eventCallback) {
var eventName = "keydown";
if (!this.events[eventName]) {
this.events[eventName] = [];
}
if (eventCallback instanceof IgniteProperty) {
this.callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => {
//Create a new wrapped function to check for the enter key being pressed.
var wrapped = (e) => {
if (e.key === 'Backspace') {
newValue(e);
}
};
eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
}));
//Create the initial wrapper
var target = eventCallback._value;
var wrapped = (e) => {
if (e.key === 'Backspace') {
target(e);
}
};
//Store the wrapped so that it's the old value next time around.
eventCallback._value = wrapped;
this.events[eventName].push(wrapped);
} else {
this.on(eventName, (e) => {
if (e.key === 'Backspace') {
eventCallback(e);
}
});
}
return this;
}
/**
* Adds a CSS property to this template with a value and priority.
* @param {String} name The name of the CSS property to set.
* @param {String|IgniteProperty} value The value to set for the property. If an IgniteProperty is used it will auto update this style.
* @param {String} priority If set to "important" then the style will be marked with !important.
* @param {String} priority If set to "important" then the style will be marked with !important. Acceptable values: important, !important, true, false, null
* @param {Function} converter Optional function to convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
style(name, value, priority = null, converter = null) {
IgniteRenderingContext.push();
//If the name has a : remove it.
if (name && typeof name === "string" && name.includes(":")) {
name = name.replace(":", "");
}
//If the priority starts with a ! remove it.
if (priority && typeof priority === "string" && priority.trim().startsWith("!")) {
priority = priority.split("!")[1].trim();
} else if (priority && typeof priority === "boolean") {
priority = "important"; //If priority is true, set it to important
} else if (!priority && typeof priority === "boolean") {
priority = null; //If priority is false, set it to null.
}
//If the value has a ; remove it.
if (value && typeof value === "string" && value.includes(";")) {
value = value.replace(";", "");
}
if (value instanceof IgniteProperty) {
this.callbacks.push(value.attachOnChange((oldValue, newValue) => this.onCssValueChanged(oldValue, newValue, name, converter)));
this.styles[name] = { name: name, value: (converter != null ? converter(value.value) : value.value), priority: priority };
this.callbacks.push(value.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, (converter ? converter(newValue) : newValue))));
this.styles[name] = { name: name, value: (converter ? converter(value.value) : value.value), priority: priority };
} else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
//There must be a converter for this to work correctly
if (!converter) {
throw "Cannot pass an array of properties without using a converter";
}
//Attack a callback for all the properties
value.forEach(prop => {
this.callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this.callbacks.push(prop.attachOnPush((oldValue, newValue) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this.callbacks.push(prop.attachOnPop((oldValue, newValue) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
});
this.styles[name] = { name: name, value: converter(...this.getValuesForProperties(value)), priority: priority };
} else {
this.styles[name] = { name: name, value: (converter != null ? converter(value) : value), priority: priority };
}
IgniteRenderingContext.pop();
return this;
}
/**
* Hides the element this template is constructing if the value is true.
* @param {Boolean} 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.
*/
hide(value, converter = null) {
return this.style("display", value, true, (...params) => {
return ((converter != null && converter(...params)) || (converter == null && params[0])) ? "none" : null;
});
}
/**
* Sets the id attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the id attribute of the element this template will construct.
@ -339,6 +480,17 @@ class IgniteTemplate {
return this.attribute("type", value, converter);
}
/**
* Sets a data attribute on the element to be constructed by this template.
* @param {String} name The name of the data attribute to set on the element this template will construct.
* @param {String|IgniteProperty} value The value to set for the data attribute of the element this template will construct.
* @param {*} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
data(name, value, converter = null) {
return this.attribute(`data-${name}`, value, converter);
}
/**
* Sets the value attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the src attribute of the element to be constructed by this template.
@ -462,6 +614,11 @@ class IgniteTemplate {
}
}
//Set the elements inner html if it was set
if (this.elementInnerHTML != null) {
this.element.innerHTML = this.elementInnerHTML;
}
//Construct the children under this element
for (var i = 0; i < this.children.length; i++) {
this.children[i].construct(this.element);
@ -515,6 +672,8 @@ class IgniteTemplate {
//Remove any refs
if (this.refs) {
//Pass null to our refs so that the reference is updated.
this.refs.forEach(ref => ref(null));
this.refs = null;
}
}
@ -529,8 +688,10 @@ class IgniteTemplate {
*/
onClassChanged(oldValue, newValue, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
oldValue = converter(oldValue);
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
var oldClasses = (oldValue != null && oldValue != "" ? oldValue.toString().split(" ") : []);
@ -566,7 +727,9 @@ class IgniteTemplate {
*/
onAttributeChanged(oldValue, newValue, attributeName, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
if (this.element) {
@ -589,7 +752,9 @@ class IgniteTemplate {
*/
onValueChanged(oldValue, newValue, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
//Only update the elements value if it actually changed.
@ -618,7 +783,9 @@ class IgniteTemplate {
*/
onPropertyChanged(oldValue, newValue, propertyName, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
if (this.element) {
@ -630,6 +797,28 @@ class IgniteTemplate {
this.properties[propertyName].value = newValue;
}
/**
* Called when the inner html for this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {Function} converter
* @ignore
*/
onInnerHTMLChanged(oldValue, newValue, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
if (this.element) {
this.element.innerHTML = newValue;
}
this.elementInnerHTML = newValue;
}
/**
* Called when a ref was changed and we need to update the refs
* value to match this elements reference.
@ -675,15 +864,14 @@ class IgniteTemplate {
/**
* Called when a css value was changed and we need to update the styling.
* @param {any} oldValue
* @param {String} name
* @param {any} newValue
* @param {any} style
* @param {Function} converter
* @ignore
*/
onCssValueChanged(oldValue, newValue, name, converter) {
if (converter != null) {
newValue = converter(newValue);
onStyleChanged(name, newValue) {
//Remove the ; from the value if there is one.
if (newValue && typeof newValue === "string" && newValue.includes(";")) {
newValue = newValue.replace(";", "");
}
if (this.element) {
@ -692,6 +880,12 @@ class IgniteTemplate {
this.styles[name].value = newValue;
}
getValuesForProperties(props) {
var ret = [];
props.forEach(prop => ret.push(prop.value));
return ret;
}
}
/**
@ -739,6 +933,8 @@ class button extends IgniteTemplate {
*/
constructor(...children) {
super("button", children);
this.type("button");
}
}
@ -802,6 +998,18 @@ class h5 extends IgniteTemplate {
}
}
/**
* An ignite template that can be used to construct a h6 element.
*/
class h6 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h6", children);
}
}
/**
* An ignite template that can be used to construct a p element.
*/
@ -942,14 +1150,20 @@ class html extends IgniteTemplate {
//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.
if (this.elements.length == 0 && this.code) {
var template = window.document.createElement("template");
template.innerHTML = this.code;
while (template.content.childNodes.length > 0) {
var item = template.content.childNodes[0];
this.element.parentElement.insertBefore(item, this.element);
this.elements.push(item);
//If the code is an ignite template then reder that template.
if (this.code instanceof IgniteTemplate) {
this.code.construct(parent, this.element);
this.elements.push(this.code.element);
} else {
var template = window.document.createElement("template");
template.innerHTML = this.code;
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();
}
template.remove();
}
}
@ -1068,7 +1282,7 @@ class list extends IgniteTemplate {
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
} else {
template.construct(this.element.parentElement, null);
template.construct(this.element.parentElement, this.element);
}
this.children.push(template);
@ -1079,9 +1293,11 @@ class list extends IgniteTemplate {
}
onListPop(oldValue, newValue) {
this.children[this.children.length - 1].deconstruct();
this.children.pop();
this.elements.pop();
if (this.children.length > 0) {
this.children[this.children.length - 1].deconstruct();
this.children.pop();
this.elements.pop();
}
}
}
@ -1230,16 +1446,11 @@ class slot extends IgniteTemplate {
/**
* Called when a css value was changed and we need to update the styling.
* @param {any} oldValue
* @param {String} name
* @param {any} newValue
* @param {any} style
* @ignore
*/
onCssValueChanged(oldValue, newValue, name, converter) {
if (converter != null) {
newValue = converter(newValue);
}
onStyleChanged(name, newValue) {
this.parent.elements.forEach((element) => {
element.style.setProperty(name, newValue, this.styles[name].priority);
});
@ -1261,6 +1472,7 @@ export {
h3,
h4,
h5,
h6,
p,
span,
i,