diff --git a/OpenScript.js b/OpenScript.js index e3764f1..c57d5ee 100644 --- a/OpenScript.js +++ b/OpenScript.js @@ -325,6 +325,11 @@ var OpenScript = { */ Emitter: class { listeners = {} + + /** + * List of emitted events + */ + emitted = {}; addListener(eventName, fn) { this.listeners[eventName] = this.listeners[eventName] || []; @@ -366,7 +371,9 @@ var OpenScript = { // Fire the event emit(eventName, ...args) { - + + this.emitted[eventName] = true; + let fns = this.listeners[eventName]; if (!fns) return false; fns.forEach((f) => { @@ -395,7 +402,7 @@ var OpenScript = { Component: class { /** - * List of events that a component emits + * List of events that the component emits */ EVENTS = { rendered: 'rendered', // component is visible on the dom @@ -404,13 +411,45 @@ var OpenScript = { mounted: 'mounted', // the component is now registered prebind: 'prebind', // the component is ready to bind bound: 'bound', // the component has bound - markupBound: 'markup-bound' // a temporary markup has bound + markupBound: 'markup-bound', // a temporary markup has bound + + beforeHidden: 'before-hidden', + hidden: 'hidden', + unmounted: 'unmounted', // removed from the markup engine memory + beforeVisible: 'before-visible', // before the markup is made visible + visible: 'visible' // the markup is now made visible } + /** * Name of the component */ name; + /** + * Has the component being mounted + */ + mounted = false; + + /** + * Has the component bound + */ + bound = false; + + /** + * Has the component rendered + */ + rendered = false; + + /** + * Has the component rerendered + */ + rerendered = false; + + /** + * Is the component visible + */ + visible = true; + /** * Anonymous component ID @@ -434,6 +473,13 @@ var OpenScript = { constructor() { this.name = this.constructor.name; + + this.emitter.once(this.EVENTS.rendered, _ => this.rendered = true); + this.emitter.on(this.EVENTS.hidden, _ => this.visible = false); + this.emitter.on(this.EVENTS.rerendered, _ => this.rerendered = true); + this.emitter.on(this.EVENTS.bound, _ => this.bound = true); + this.emitter.on(this.EVENTS.mounted, _ => this.mounted = true); + this.emitter.on(this.EVENTS.visible, _ => this.visible = true); } /** @@ -450,6 +496,19 @@ var OpenScript = { this.emit(this.EVENTS.mounted); } + /** + * Deletes all the component's markup from the DOM + */ + unmount(){ + let all = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}}`); + + for(let elem of all) { + elem.remove(); + } + + return true; + } + /** * Emits an event * @param {string} event @@ -474,6 +533,7 @@ var OpenScript = { for(let elem of all) { let hId = elem.getAttribute('ojs-key'); + let args = [...h.compArgs.get(hId)]; this.wrap(...args, {parent: elem, replaceParent: true}); @@ -486,6 +546,89 @@ var OpenScript = { return true; } + /** + * Converts camel case to kebab case + * @param {string} name + */ + kebab(name){ + let newName = ""; + + for(const c of name){ + if(c.toLocaleUpperCase() === c && newName.length > 1) newName += "-"; + newName += c.toLocaleLowerCase(); + } + + return newName; + } + + /** + * Hides all the markup of this component + * @emits before-hidden + * @emits hidden + * @returns {bool} + */ + hide(){ + this.emit(this.EVENTS.beforeHidden); + + let all = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}`); + + for(let elem of all) { + elem.style.display = 'none'; + } + + this.emit(this.EVENTS.hidden); + + return true; + } + + /** + * Remove style-display-none from all this component's markup + * @emits before-visible + * @emits visible + * @returns bool + */ + show() { + this.emit(this.EVENTS.beforeVisible); + + let all = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}}`); + + for(let elem of all) { + elem.style.display = ''; + } + + this.emit(this.EVENTS.visible); + + return true; + } + + /** + * Ensure that the action will get called + * even if the event was emitted previous + * @param {string} event + * @param {...function} listeners + */ + onAll(event, ...listeners) { + + // check if we have previously emitted this event + listeners.forEach(a => { + if(this.emitter.emitted[event]) a(); + + this.emitter.on(event, a); + }); + } + + /** + * Add Event Listeners to that component + * @param {string} event + * @param {...function} listeners + */ + on(event, ...listeners) { + // check if we have previously emitted this event + listeners.forEach(a => { + this.emitter.on(event, a); + }); + } + /** * Gets all the listeners for itself and adds them to itself */ @@ -496,13 +639,14 @@ var OpenScript = { let events = h.eventsMap.get(this.name); for(let event in events) { + + events[event].forEach((listener) => { + let func = listener.function; - if(!this.emitter.listeners[event]) { - this.emitter.listeners[event] = events[event]; - continue; - } + if(listener.type === 'all') this.onAll(event, func); + else this.on(event, func); + }); - this.emitter.listeners[event].push(...events[event]); } h.eventsMap.delete(this.name); @@ -575,7 +719,7 @@ var OpenScript = { delete args[index]; - let current = h.dom.querySelectorAll(`ojs-${this.name.toLowerCase()}[s-${state.id}="${state.id}"]`) ?? []; + let current = h.dom.querySelectorAll(`ojs-${this.kebab(this.name)}[s-${state.id}="${state.id}"]`) ?? []; current.forEach(e => { e.textContent = ""; @@ -598,10 +742,13 @@ var OpenScript = { attr[`s-${s.id}`] = s.id; }); - return h[`ojs-${this.name}`](attr, this.render(...args), { + if(!this.visible) attr.style = 'display: none;'; + + return h[`ojs-${this.kebab(this.name)}`](attr, this.render(...args), { component: this, event: this.EVENTS.rendered, - eventParams: []}); + eventParams: [] + }); } /** @@ -1174,18 +1321,82 @@ var OpenScript = { } /** + * Deletes the component from the Markup Engine Map. * @emits unmount * Removes an already registered company * @param {string} name + * @param {string} withMarkup remove the markup of this component + * as well. * @returns {boolean} */ - deleteComponent(name){ - this.compMap.get(name) - .emitter - .emit('unmount', this.compMap.get(name)); + deleteComponent = (name, withMarkup = true) => { + + if(!this.has(name)){ + console.info(`Trying to delete an unregistered component {${name}}. Please ensure that the component is registered before deleting it.`); + + return false; + } + + if(withMarkup) this.getComponent(name).unmount(); + + this.getComponent(name) + .emit('unmount'); return this.compMap.delete(name); } + + /** + * Checks if a component is registered with the + * markup engine. + * @param {string} name + * @returns + */ + has = (name) => { + return this.compMap.has(name); + } + + /** + * Checks if a component is registered + * @param {string} name + * @param {string} method method name + * @returns + */ + isRegistered = (name, method = 'access') => { + if(this.has(name)) return true; + + console.info(`Trying to ${method} an unregistered component {${name}}. Please ensure that the component is registered by using h.has(componentName)`); + + return false; + } + + /** + * Removes all a component's markup + * from the DOM + * @param {string} name + */ + hide = (name) => { + + if(!this.isRegistered(name, 'hide')) return false; + + const c = this.getComponent(name); + c.hide(); + + return true; + } + + /** + * make all the component visible + * @param {string} name component name + * @returns + */ + show = (name) => { + if(!this.isRegistered(name, 'show')) return false; + + const c = this.getComponent(name); + c.show(); + + return true; + } /** * handles the DOM element creation @@ -1380,6 +1591,21 @@ var OpenScript = { return final; } + __addToEventsMap = (component, event, listeners) => { + + if(!this.eventsMap.has(component)){ + this.eventsMap.set(component, {}); + this.eventsMap.get(component)[event] = listeners; + return; + } + + if(!this.eventsMap.get(component)[event]) { + this.eventsMap.get(component)[event] = []; + } + + this.eventsMap.get(component)[event].push(...listeners); + } + /** * Adds an event listener to a component * @param {string|Array} component component name @@ -1400,27 +1626,55 @@ var OpenScript = { event = tmp[1]; } - if(this.compMap.has(component)) { + + if(this.has(component)) { - if(!this.emitter(component).listeners[event]) { - this.emitter(component).listeners[event] = []; - } + this.getComponent(component) + .on(event, listeners); - this.emitter(component).listeners[event].push(...listeners); continue; } - - if(!this.eventsMap.has(component)){ - this.eventsMap.set(component, {}); - this.eventsMap.get(component)[event] = listeners; - continue; + + listeners.forEach((f, i) => { + listeners[i] = {type: 'after', function: f}; + }); + + this.__addToEventsMap(component, event, listeners); + } + } + + /** + * Add events listeners to a component that will + * execute even after the event has been emitted + * @param {string|Array} component + * @param {string} event + * @param {...function} listeners + */ + onAll = (component, event, ...listeners) => { + let components = component; + + if(!Array.isArray(component)) components = [component]; + + for(let component of components) { + + if(/\./.test(component)){ + let tmp = component.split('.').filter(e => e); + component = tmp[0]; + listeners.push(event); + event = tmp[1]; } - if(!this.eventsMap.get(component)[event]) { - this.eventsMap.get(component)[event] = []; + if(this.has(component)) { + this.getComponent(component) + .onAll(event, listeners); + continue; } - this.eventsMap.get(component)[event].push(...listeners); + listeners.forEach((f, i) => { + listeners[i] = {type: 'all', function: f}; + }); + + this.__addToEventsMap(component, event, listeners); } } diff --git a/example/logic.js b/example/logic.js index 8c82c5f..ca90930 100644 --- a/example/logic.js +++ b/example/logic.js @@ -1,7 +1,7 @@ ojs(async e => { fetchContext("rootCxt", "RootContext"); - putContext("blogCxt", "Blog.Context"); + fetchContext("blogCxt", "Blog.Context"); req("App");