import { IgniteProperty, IgniteCallback, IgniteRendering } from './ignite-html.js'; import { IgniteTemplate } from './ignite-template.js'; /** * The outline of a Ignite Element that extends the html element * and can be used to create custom components. Ignite Element's use an Ignite Template * for the render function. * * @example * * class MainApp extends IgniteElement { * constructor() { * super(); * } * * get properties() { * return { * }; * } * * render() { * return this.template * .child( * new h1(` Ignite HTML`), * new h4(`Adding more fire to the web.`) * ); * } * } * * customElements.define("main-app", MainApp); * * @example * //If you want to easily use an Ignite Element with templates see the following which can be added * //to any component file * class MainAppTemplate extends IgniteTemplate { * constructor(...children) { * super("main-app", children); * } * } * * export { * MainAppTemplate as MainApp * } * * @example * //If a template was created for a Ignite Element (like the previous example) it can be used just like any other template: * new MainApp() */ class IgniteElement extends HTMLElement { constructor() { super(); /** * @ignore * @type {Boolean} * */ this.elementConnected = false; //Used to know if the connectedCallback was already called. /** * The ignite html template used to construct this element. * @type {IgniteTemplate} * */ this.template = null; /** * The child elements within this element upon creation. * @type {HTMLElement[]} * */ this.elements = []; /** * @ignore * @type {IgniteCallback} * */ this.readyCallback = new IgniteCallback(() => this.ready()); /** * @ignore * @type {IgniteCallback[]} * */ this.onDisconnectCallbacks = []; //Create the variables for this element. this.createVariables(); //Create the properties for this element. this.createProperties(); //Init the element before connected callback is fired //Create a new rendering context so the init method can access properties correctly. IgniteRendering.push(); this.init(); IgniteRendering.pop(); } /** * Returns an object with all the variables for this ignite element. Variables * unlike properties have no callbacks or events, they are just data. * * @returns {Object} An object with properties that will be assigned to this ignite element as variables. * * @example * get variables() { * return { * dialog: null, * clickCallback: null * }; * } */ get variables() { return {}; } /** * Returns an object with all the properties for this ignite element. If null or empty * then no properties will be created. To change a property and read it's current value see below. * * @returns {Object} An object with properties that will be assigned to this ignite element. * * @example * get properties() { * return { * show: false, * title: "This is a title", * items: [1, 2, 3] * }; * } * * @example * //To change a property access it via this.PROP_NAME * this.show = false; * * //To get a properties value access if via this.PROP_NAME * console.log(this.title); */ get properties() { return {}; } /** * Returns any CSS styling code for this ignite element. If this returns a non null * value the CSS will auto be injected into the current HTML page once and reused for the life * of the ignite element. * * @returns {String} A string containing CSS code to be used with this ignite element. * * @example * get styles() { * return ` * h1 { * color: black; * } * `; * } */ get styles() { return null; } /** * Creates the variables for this ignite element and initializes them. * @ignore */ createVariables() { var vars = this.variables; if (vars != null) { Object.keys(vars).forEach(name => this[name] = vars[name]); } } /** * Resets the variables for this element back to their original default * values. */ resetVariables() { var vars = this.variables; if (vars != null) { Object.keys(vars).forEach(name => this[name] = vars[name]); } } /** * Gets all the property values from this element and returns it. * @returns {Object} An object with all of the property values for this ignite element. */ getProperties() { var ret = {}; var props = this.properties; IgniteRendering.push(); Object.keys(props).forEach(name => ret[name] = this[name]); IgniteRendering.pop(); return ret; } /** * Sets all the property values on this element. */ setProperties(props) { if (props == null || props == undefined) { return; } if (props instanceof IgniteObject) { props.update(); Object.getOwnPropertyNames(props).forEach(name => this[name] = props[name]); } else { Object.keys(props).forEach(name => this[name] = props[name]); } } /** * Creates the getters/setters for properties in this * ignite element and initializes everything. * @ignore */ createProperties() { var props = this.properties; if (props != null) { Object.keys(props).forEach(name => { var prop = (props[name] instanceof IgniteProperty ? props[name] : new IgniteProperty(props[name])); Object.defineProperty(this, name, { get: () => { return (IgniteRendering.rendering ? prop : prop.value); }, set: (value) => { prop.value = value; } }); }); } } /** * Resets the properties for this element back to their original default * values. */ resetProperties() { var props = this.properties; if (props != null) { Object.keys(props).forEach(name => this[name] = props[name]); } } /** * Setups this ignite element and constructs it's template when * this function is called by the DOM upon this element being created somewhere. * @ignore */ connectedCallback() { //Only run this if we haven't been connected before. //If this element is moved the instance will be the same but it will call this again //and we want to trap this and not run the code below twice. if (this.elementConnected) { return; } else { this.elementConnected = true; } //See if a styling sheet has been created for this element if it's needed. if (this.styles !== null && this.styles !== "") { if (document.getElementsByClassName(`ignite-html-${this.tagName.toLowerCase()}-css`).length == 0) { var styleEl = document.createElement("style"); styleEl.classList.add(`ignite-html-${this.tagName.toLowerCase()}-css`); styleEl.innerHTML = this.styles; document.body.prepend(styleEl); } } //If we don't already have a template, make sure we create one, //this can happen if this element was constructed in the DOM instead of within a template. if (!this.template) { this.template = new IgniteTemplate(); this.template.element = this; this.template.tagName = this.tagName; } //Add any childNodes we have to the elements list within this this.childNodes.forEach((item) => this.elements.push(item)); //Enter a rendering context so properties don't expose their values until we are done. IgniteRendering.enter(); //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) { throw `RenderTemplate was null for template: ${this.tagName}. Is render() returning null or not returning a template?`; } //Construct our template. try { this.template.construct(this.parentElement); } catch (error) { console.error(error); } //Leave the rendering context. IgniteRendering.leave(); //Invoke the after render function, ensure we are not in a rendering context. IgniteRendering.push(); this.afterRender(); IgniteRendering.pop(); //Let the rendering context know this element is ready. IgniteRendering.ready(this.readyCallback); } /** * Cleanups this element and deconstructs everything when this element is removed from * the DOM. * @ignore */ disconnectedCallback() { //Run a test here, if the element was moved but not removed it will have a disconnect //get called but then be reconnected, so use a timer to see if this is what happened. setTimeout(() => { //If the element is connected then don't do this, more than likely //the element was moved or something. if (this.isConnected) { return; } //If we still have a reference to our template, deconstruct it. if (this.template) { this.template.deconstruct(); } //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. * * @returns An ignite template to be used to construct this ignite element. * * @see this.template is automatically created for each IgniteElement and must be used in the render() function. * * @example * render() { * return this.template * .child( * new h1(` Ignite HTML`), * new h4(`Adding more fire to the web.`) * ); * } */ render() { return null; } /** * Called right after this element is rendered. * Note: It's not guaranteed this element is connected to the DOM yet, but it will at least be fully initialized. */ afterRender() { } /** * Called when this ignite element is being initialized. When this is called * the element has not been created. This is good for login checking code or special setup code. */ init() { } /** * Called when this ignite element is ready. This will invoke once all * ignite elements on the current page have been rendered and setup. You can safely know * all elements have be initialized and are ready once this is called. */ ready() { } /** * Called when this ignite element should cleanup * any resources like events, timers, ect. This is called when the element * is being destroyed. */ cleanup() { } /** * Generates a uuid and returns it. * @returns {String} A unique string, for example: '1b23ec67-4d90-4992-9c5a-b5c0844deaef' */ uuid() { return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ); } } export { IgniteElement };