UNPKG

@xeito/core

Version:

Core modules for Xeito | Framework for building web applications

891 lines (871 loc) 27.2 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; // packages/core/index.ts import { render as render2, html as html4, svg, Hole as Hole3 } from "uhtml"; // packages/core/components/xt-for-component.ts import { html } from "uhtml"; // packages/core/classes/xeito-component.ts import { render } from "uhtml"; var XeitoComponent = class extends HTMLElement { constructor() { super(); /** * Xeito internals object * Will be populated by the @Component decorator * with the component's metadata */ this._XeitoInternals = {}; this._state = /* @__PURE__ */ new Map(); this._props = /* @__PURE__ */ new Map(); // Action controls this._IActionIndex = 0; this._actionInstances = []; // Pipe controls this._IPipeIndex = 0; this._pipeInstances = []; // Store controls this._stores = /* @__PURE__ */ new Map(); this._storeSubscriptions = /* @__PURE__ */ new Map(); // Dirty flag to manage batched updates this._dirty = false; /** * Slot Content * Will be populated by the constructor * It will contain the slotted content of the component * eg: <xeito-component><div slot="header">Header</div></xeito-component> * slotContent.header will contain the div element * This can be accessed inside the render method * eg: html`<div>${this.slotContent.header}</div>` */ this.slotContent = {}; this._XeitoInternals = Object.assign({}, this.constructor.prototype._XeitoInternals); this.slotContent = this.getSlotContent(); this.global = this._XeitoInternals.global; this._XeitoInternals.shadow = this._XeitoInternals.shadow ?? this.global.config.shadow; this.assignChildrenGlobal(); let DOMRoot = this; if (this._XeitoInternals.shadow === true) { this.attachShadow({ mode: "open" }); DOMRoot = this.shadowRoot; } this._DOMRoot = DOMRoot; this.onInit(); } /** * Native connectedCallback * Will be called when the component is connected to the DOM * */ connectedCallback() { this.onWillMount(); this.bindMethods(); this._update(); this.onDidMount(); } /** * Get the slotted content of the component * @returns { Record<string, any> } Slot content object */ getSlotContent() { const slotContent = { default: [] }; const children = Array.from(this.childNodes); if (children) { for (let child in children) { const childEl = children[child]; if (childEl.nodeType === Node.TEXT_NODE || childEl.nodeType === Node.COMMENT_NODE) { slotContent.default.push(childEl); } else { const slot = childEl.getAttribute("slot"); if (slot) { if (!slotContent[slot]) slotContent[slot] = []; slotContent[slot].push(childEl); } else { if (!slotContent.default) slotContent.default = []; slotContent.default.push(childEl); } } } } return slotContent; } /** * Assign children global * Assigns the global property to the children of the component */ assignChildrenGlobal() { this._XeitoInternals.imports?.forEach((child) => { child.prototype._XeitoInternals.global = this.global; }); } /** * Bind methods * Binds the methods of the component to the component itself * This is done to avoid having to wrap the methods in arrow functions * eg: onClick={this.onClick} instead of onClick={() => this.onClick()} * NOTE: Arrow functions are still required to pass data to the method or native events * eg: onClick={() => this.onClick(data)} or onClick={(e) => this.onClick(e)} */ bindMethods() { const methods = Object.getOwnPropertyNames(this.constructor.prototype); methods.forEach((method) => { if (method !== "constructor" && method !== "render" && typeof this[method] === "function") { this[method] = this[method].bind(this); } }); } /** * Request an update of the component * This will schedule an update of the template (batching updates by using a promise) */ requestUpdate() { if (!this._dirty) { this._dirty = true; queueMicrotask(() => { this._update(); this._dirty = false; }); } } /** * Force an update of the component (no batching) * This will update the template immediately without batching * This should be used with caution as it can cause performance issues */ forceUpdate() { this._update(); } /** * Update the component * This will render the template and update the DOM * It will also reset the pipe index before rendering */ _update() { this._IPipeIndex = -1; this._template = render(this._DOMRoot, this.render()); } /** * Sets a state value and triggers an update if needed * @param key Key of the state to set * @param value Value to set * @param triggerUpdate Whether to trigger an update or not */ setState(key, value) { if (this._state.has(key)) { if (this._state.get(key) === value) return; this._state.set(key, value); this._watchers?.get(key)?.forEach((watcher) => { this[watcher]({ name: key, value }); }); this.requestUpdate(); } else { this._state.set(key, value); } } /** * Returns the value of a state key * @param key Key of the state to get * @returns Value of the state key */ getState(key) { return this._state.get(key); } /** * Sets a prop value and triggers an update * @param key Key of the prop to set * @param value Value to set */ setProp(key, value) { if (this._props.has(key)) { const change = { name: key, oldValue: this._props.get(key), newValue: value }; this.onPropChange(change); this._props.set(key, value); this._watchers?.get(key)?.forEach((watcher) => { this[watcher]({ name: key, value }); }); this.requestUpdate(); } else { this._props.set(key, value); } } /** * Returns the value of a prop key * @param key Key of the prop to get * @returns Value of the prop key */ getProp(key) { return this._props.get(key); } /** * Sets a store and subscribes to it to trigger updates when it changes * @param key Key of the property that contains the store * @param store The store to set */ setStore(key, store) { const subscription = store.subscribe(() => { if (!this._stores.has(key)) { this._stores.set(key, store); } else { this.requestUpdate(); } }); this._storeSubscriptions.set(key, subscription); } /** * Returns the store for the given key * @param key Key of the property that contains the store * @returns The store */ getStore(key) { return this._stores.get(key); } /** * Use method (Use an action inside the component) * The 'use' method is used to provide actions to the template * Actions have to be provided in the Component decorator options * eg: @Component({ actions: [Action1, Action2] }) * @param selector * @param args * @returns */ use(selector, ...args) { return (e) => { this._IActionIndex++; if (this._actionInstances[this._IActionIndex]) { this._actionInstances[this._IActionIndex].clean(); return this._actionInstances[this._IActionIndex].update(e.parentElement, ...args); } else { let action = this._XeitoInternals.actions?.find((action2) => action2.selector === selector); if (!action) { action = this.global.actions?.find((action2) => action2.selector === selector); } if (action) { this._actionInstances[this._IActionIndex] = new action(); return this._actionInstances[this._IActionIndex].update(e.parentElement, ...args); } else { throw new Error(`Action '${selector}' not found in component '<${this._XeitoInternals.selector}>', did you forget to add it to the actions array or install the plugin?`); } } }; } /** * Pipe method (Use a pipe inside the component) * The 'pipe' method is used to provide pipes to the template * Pipes have to be provided in the Component decorator options * eg: @Component({ pipes: [Pipe1, Pipe2] }) * @param selector * @param args * @returns */ pipe(selector, ...args) { return () => { this._IPipeIndex++; if (this._pipeInstances[this._IPipeIndex]) { return this._pipeInstances[this._IPipeIndex].update(...args); } else { let pipe = this._XeitoInternals.pipes?.find((pipe2) => pipe2.selector === selector); if (!pipe) { pipe = this.global.pipes?.find((pipe2) => pipe2.selector === selector); } if (pipe) { this._pipeInstances[this._IPipeIndex] = new pipe(); return this._pipeInstances[this._IPipeIndex].update(...args); } else { throw new Error(`Pipe '${selector}' not found in component '<${this._XeitoInternals.selector}>', did you forget to add it to the pipes array or install the plugin?`); } } }; } /** * Native attributeChangedCallback * Calls the onChanges method and requests an update of the component * @param name * @param oldValue * @param newValue */ attributeChangedCallback(name, oldValue, newValue) { this.setProp(name, newValue); } /** * Native disconnectedCallback * Calls the onWillUnmount method */ disconnectedCallback() { this.onUnmount(); this._actionInstances.forEach((action) => action.clean()); this._pipeInstances.forEach((pipe) => pipe.clean()); this._storeSubscriptions.forEach((subscription) => subscription.unsubscribe()); } /** * Render method desgin to be overriden by the user */ render() { } /** * Lifecycle methods desgin to be overriden by the user * onInit: Called when the component is initialized (constructor) * onWillMount: Called before the first render (connectedCallback) * onDidMount: Called after the first render (connectedCallback) * onUnmount: Called when the component is unmounted (disconnectedCallback) */ onInit() { } onWillMount() { } onDidMount() { } onUnmount() { } /** * On changes method desgin to be overriden by the user * Gets called when an attribute/property changes (attributeChangedCallback) * @param { PropChange } change Prop changes object */ onPropChange(change) { } }; // packages/core/functions/validate-selector.ts var elementRegex = /^[a-z](?:[\.0-9_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*-(?:[\x2D\.0-9_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/; var isPotentialCustomElementName = function(string) { return elementRegex.test(string); }; var reservedNames = /* @__PURE__ */ new Set([ "annotation-xml", "color-profile", "font-face", "font-face-src", "font-face-uri", "font-face-format", "font-face-name", "missing-glyph" ]); function hasError(name) { if (!name) { return "Missing element name."; } if (/[A-Z]/.test(name)) { return "Custom element names must not contain uppercase ASCII characters."; } if (!name.includes("-")) { return "Custom element names must contain a hyphen. Example: unicorn-cake"; } if (/^\d/i.test(name)) { return "Custom element names must not start with a digit."; } if (/^-/i.test(name)) { return "Custom element names must not start with a hyphen."; } if (!isPotentialCustomElementName(name)) { return "Invalid element name."; } if (reservedNames.has(name)) { return "The supplied element name is reserved and can't be used.\nSee: https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name"; } } function hasWarning(name) { if (/^polymer-/i.test(name)) { return "Custom element names should not start with `polymer-`.\nSee: http://webcomponents.github.io/articles/how-should-i-name-my-element"; } if (/^x-/i.test(name)) { return "Custom element names should not start with `x-`.\nSee: http://webcomponents.github.io/articles/how-should-i-name-my-element/"; } if (/^ng-/i.test(name)) { return "Custom element names should not start with `ng-`.\nSee: http://docs.angularjs.org/guide/directive#creating-directives"; } if (/^xml/i.test(name)) { return "Custom element names should not start with `xml`."; } if (/^[^a-z]/i.test(name)) { return "This element name is only valid in XHTML, not in HTML. First character should be in the range a-z."; } if (name.endsWith("-")) { return "Custom element names should not end with a hyphen."; } if (/\./.test(name)) { return "Custom element names should not contain a dot character as it would need to be escaped in a CSS selector."; } if (/[^\u0020-\u007E]/.test(name)) { return "Custom element names should not contain non-ASCII characters."; } if (/--/.test(name)) { return "Custom element names should not contain consecutive hyphens."; } if (/[^a-z\d]{2}/i.test(name)) { return "Custom element names should not contain consecutive non-alpha characters."; } } var validateSelector = (selector) => { const errorMessage = hasError(selector); return { isValid: !errorMessage, message: errorMessage || hasWarning(selector) }; }; // packages/core/decorators/component.ts function Component(componentMetadata) { return function ComponentDecorator(constructor) { if (!componentMetadata.selector) { throw new Error("Component selector is required"); } const result = validateSelector(componentMetadata.selector); if (!result.isValid) { throw new Error(`Component selector '${componentMetadata.selector}' is invalid: ${result.message}`); } else { if (result.message) { console.warn(`Component selector '${componentMetadata.selector}' is not recommended: ${result.message}`); } } constructor.prototype._XeitoInternals = { selector: componentMetadata.selector, actions: componentMetadata.actions || [], pipes: componentMetadata.pipes || [], imports: componentMetadata.imports || [], services: componentMetadata.services || [], shadow: componentMetadata.shadow ?? false, DOMRoot: null, template: null, global: null }; if (!customElements.get(componentMetadata.selector)) { customElements.define(componentMetadata.selector, constructor); } else { console.warn(`Component '${componentMetadata.selector}' already registered`); } return constructor; }; } // packages/core/decorators/prop.ts function Prop() { return function _PropDecorator(target, key) { Object.defineProperty(target, key, { get() { return this.getProp(key); }, set(value) { const oldValue = this.getProp(key); if (oldValue === value) return; this.setProp(key, value); } }); const observedAttributes = target.constructor.observedAttributes || []; if (!observedAttributes.includes(key)) { observedAttributes.push(key); target.constructor.observedAttributes = observedAttributes; } else { console.warn(`Attribute '${key}' is already observed in component '<${target._XeitoInternals.selector}>'.`); } }; } // packages/core/decorators/watch.ts function Watch(...propertyNames) { return function _WatchDecorator(target, key, descriptor) { Object.defineProperty(target, key, { set() { if (!this._watchers) this._watchers = /* @__PURE__ */ new Map(); propertyNames.forEach((propertyName) => { if (!this._watchers.has(propertyName)) this._watchers.set(propertyName, []); this._watchers.get(propertyName).push(key); }); Object.defineProperty(target, key, { value: descriptor.value }); } }); target[key] = descriptor.value; }; } // packages/core/components/xt-for-component.ts var XtForComponent = class extends XeitoComponent { onWillMount() { this.computeTemplate(); } itemsChanged() { this.computeTemplate(); } computeTemplate() { this.listTemplate = this.of.map((item, index) => { return this.each(item, index); }); } render() { return html` ${this.listTemplate} `; } }; __decorateClass([ Prop() ], XtForComponent.prototype, "of", 2); __decorateClass([ Prop() ], XtForComponent.prototype, "each", 2); __decorateClass([ Watch("of") ], XtForComponent.prototype, "itemsChanged", 1); XtForComponent = __decorateClass([ Component({ selector: "xt-for" }) ], XtForComponent); // packages/core/components/xt-if-component.ts import { html as html2 } from "uhtml"; var XtIfComponent = class extends XeitoComponent { render() { return html2` ${this.when ? html2` ${this.slotContent.default} ` : null} ${!this.when && this.slotContent.else ? html2` ${this.slotContent.else} ` : null} `; } }; __decorateClass([ Prop() ], XtIfComponent.prototype, "when", 2); XtIfComponent = __decorateClass([ Component({ selector: "xt-if" }) ], XtIfComponent); // packages/core/components/xt-switch-component.ts import { html as html3 } from "uhtml"; var XtSwitchComponent = class extends XeitoComponent { constructor() { super(...arguments); this.parsedContent = { default: [] }; } onWillMount() { this.parseContent(); } parseContent() { const children = Array.from(this.children); children.forEach((child) => { const condition = child.case; if (condition && condition !== "default") { this.parsedContent[condition] = child; } else { this.parsedContent.default.push(child); } }); } render() { return html3` ${this.parsedContent[this.of] || this.parsedContent.default} `; } }; __decorateClass([ Prop() ], XtSwitchComponent.prototype, "of", 2); XtSwitchComponent = __decorateClass([ Component({ selector: "xt-switch" }) ], XtSwitchComponent); // packages/core/classes/xeito.ts var Xeito = class { constructor(rootComponent) { this.plugins = []; this.global = { properties: {}, components: [], actions: [], pipes: [] }; this.global.components.push(rootComponent); this.global.components.push(XtForComponent); this.global.components.push(XtIfComponent); this.global.components.push(XtSwitchComponent); } /** * * @param rootElement The root element to render the root component in (can be a string selector) */ bootstrap(rootElement) { let element = rootElement; if (typeof rootElement === "string") { element = document.querySelector(rootElement); } this._rootElement = element; this.attachGlobal(); render2(this._rootElement, new this.global.components[0]()); } /** * Register a plugin to the Xeito instance * @param plugin The plugin class to register * @param options The options to pass to the plugin install method */ usePlugin(plugin, options) { const pluginInstance = new plugin(this); pluginInstance.install(options); this.plugins.push(pluginInstance); } /** * Attaches the global object to all the global components * Including the root component (which is a global component) * This is called during the bootstrap process */ attachGlobal() { this.global.components.forEach((component) => { component.prototype._XeitoInternals.global = this.global; }); } }; // packages/core/classes/xeito-plugin.ts var XeitoPlugin = class { constructor(xeito) { this._xeito = xeito; } /** * Install method called by Xeito when the plugin is registered * Designed to be overriden by the plugin author * @param {any} options The options passed to the plugin when it was registered */ install(options) { } /** * Register a global action that can be used in any component * @param {string} selector The selector of the action * @param {any} action The action function */ registerGlobalAction(action) { this._xeito.global.actions.push(action); } /** * Register a global property that can be used in any component * @param {string} selector The selector of the property * @param {any} property The property value */ registerGlobalProperty(selector, property) { this._xeito.global.properties[selector] = property; } /** * Register a global pipe that can be used in any component * @param {string} selector The selector of the pipe * @param {any} pipe The pipe function */ registerGlobalPipe(pipe) { this._xeito.global.pipes.push(pipe); } /** * Register a global component that can be used in any component without importing it */ registerGlobalComponent(component) { if (!component.prototype.attachShadow) { throw new Error(`Invalid component, did you forget to extend XeitoElement in global component?`); } if (!component.prototype._XeitoInternals) { throw new Error(`Invalid component, did you forget to add the @Component decorator in global component?`); } this._xeito.global.components.push(component); } }; // packages/core/decorators/action.ts function Action(actionMetadata) { if (!actionMetadata.selector) throw new Error("Action selector is required"); return function _ActionDecorator(constructor) { constructor.selector = actionMetadata.selector; return class ActionClass extends constructor { constructor() { super(...arguments); this.selector = actionMetadata.selector; } update(element, ...args) { this["setup"](element, ...args); } clean() { this["cleanup"] && this["cleanup"](); } }; }; } // packages/core/classes/emitter.ts var Emitter = class { constructor(clazz, eventConfig) { this.clazz = clazz; this.eventConfig = eventConfig; } emit(value) { const event = new CustomEvent(this.eventConfig.name, { detail: value, composed: this.eventConfig.composed, bubbles: this.eventConfig.bubbles ?? true, cancelable: this.eventConfig.cancelable }); this.clazz.dispatchEvent(event); } }; // packages/core/decorators/event.ts function Event(eventConfig) { return function _EventDecorator(target, key) { const config = eventConfig ?? {}; config.name = config.name ?? key; Object.defineProperty(target, key, { get: function() { return new Emitter(this, config); }, set: function(value) { throw new Error(`Cannot set property '${key}' of '${target.constructor.name}' to '${value}' because it's an EventEmitter`); } }); }; } // packages/core/decorators/global.ts function Global(propertyName) { return function _GlobalDecorator(target, key) { Object.defineProperty(target, key, { get: function() { if (!this.global) { console.warn(` Cannot get the value of the global property '${key}' from component <${this._XeitoInternals.selector}>. The global object is not attached to the component. Did you try to get the value of the global property from the constructor? If so, you should get the value from the onCreate() hook instead. `); } if (propertyName) return this.global?.properties[propertyName]; return this.global?.properties[key]; }, set: function(value) { console.warn(`Cannot set the value of the global property '${key}' from a component.`); } }); }; } // packages/core/decorators/pipe.ts function Pipe(pipeMetadata) { if (!pipeMetadata.selector) throw new Error("Pipe selector is required"); return function _PipeDecorator(constructor) { constructor.selector = pipeMetadata.selector; return class PipeClass extends constructor { constructor() { super(...arguments); this.selector = pipeMetadata.selector; } update(value, ...args) { if (value !== this.previousValue || args !== this.previousArgs) { this.previousValue = value; this.previousArgs = args; this.previousResult = this["transform"](value, ...args); return this.previousResult; } else { return this.previousResult; } } clean() { this.previousValue = null; this.previousArgs = null; this.previousResult = null; this["destroy"] && this["destroy"](); } }; }; } // packages/core/decorators/ref.ts function Ref() { return function _RefDecorator(target, key) { let _val = target[key] ?? {}; Object.defineProperty(target, key, { get: function() { return _val; }, set: function(value) { _val = value; } }); }; } // packages/core/functions/decorate-store.ts function decorateStore(clazz, key, store) { clazz.setStore(key, store); Object.defineProperty(clazz, key, { get() { return clazz.getStore(key); }, enumerable: true, configurable: true }); } // packages/core/decorators/state.ts function State() { return function _StateDecorator(target, key) { Object.defineProperty(target, key, { get() { return this.getState(key); }, set(value) { if (value?.subscribe instanceof Function) { decorateStore(this, key, value); } else { this.setState(key, value); } }, enumerable: true, configurable: true }); }; } export { Action, Component, Event, Global, Hole3 as Hole, Pipe, Prop, Ref, State, Watch, Xeito, XeitoComponent, XeitoPlugin, html4 as html, render2 as render, svg }; /** * Checks if a selector is a potential custom element name. * Using logic from is-potential-custom-element-name * @see https://github.com/mathiasbynens/is-potential-custom-element-name * * @author Mathias Bynens * @license MIT * * All credit to Mathias Bynens for the original code. */ /** * Validates a custom element name. * @see https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name * * Using logic from validate-element-name.js * @see https://github.com/sindresorhus/validate-element-name * * @author Sindre Sorhus * @license MIT * * All credit to Sindre Sorhus for the original code. */ //# sourceMappingURL=index.js.map