UNPKG

yuzu-application

Version:
619 lines (541 loc) 16.6 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var invariant = _interopDefault(require('tiny-invariant')); var yuzuUtils = require('yuzu-utils'); var yuzu = require('yuzu'); function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } // A type of promise-like that resolves synchronously and supports only one observer var _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? Symbol.iterator || (Symbol.iterator = /*#__PURE__*/ Symbol("Symbol.iterator")) : "@@iterator"; // Asynchronously iterate through an object's values var _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? Symbol.asyncIterator || (Symbol.asyncIterator = /*#__PURE__*/ Symbol("Symbol.asyncIterator")) : "@@asyncIterator"; // Asynchronously iterate on a value using it's async iterator if present, or its synchronous iterator if missing function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } // Asynchronously await a promise and pass the result to a finally continuation /** * ```js * createContext([data]) * ``` * * Returns a new context object * * @param {object} data Context internal data * @return {Context} * @example * const context = createContext({ theme: 'dark' }); */ var createContext = function createContext(data) { if (data === void 0) { data = {}; } var $data = data; /** * @typedef Context * @name Context * @type {Object} */ var $ctx = { /** * ```js * getData() * ``` * Returns the context internal data. * * @memberof Context * @return {object} * @example * const context = createContext({ theme: 'dark' }); * context.getData().theme === 'dark'; */ getData: function getData() { return _extends({}, $data); }, /** * ```js * update(data) * ``` * * Replaces the context internal data. * * @memberof Context * @param {object} payload * @example * const context = createContext({ theme: 'dark' }); * * context.update({ theme: 'light ' }); * context.getData().theme === 'light'; */ update: function update(payload) { $data = _extends({}, payload); }, /** * ```js * inject(component) * ``` * * Attaches the context data to a `$context` property of the passed-in component instance * * @memberof Context * @param {Component} instance Component instance * @example * const context = createContext({ theme: 'dark' }); * const instance = new Component(); * * context.inject(instance); * * instance.$context.theme === 'dark'; */ inject: function inject(instance) { var _this = this; return Object.defineProperty(instance, '$context', { enumerable: false, get: function get() { return _this.getData(); } }); } }; return $ctx; }; var nextSbUid = /*#__PURE__*/ yuzuUtils.createSequence(); var nextChildUid = /*#__PURE__*/ yuzuUtils.createSequence(); /** * A sandbox can be used to initialize a set of components based on an element's innerHTML. * * Lets say we have the following component: * * ```js * class Counter extends Component { * static root = '.Counter'; * * // other stuff here ... * } * ``` * * We can register the component inside a sandbox like this: * * ```js * const sandbox = new Sandbox({ * components: [Counter], * id: 'main', // optional * }); * * sandbox.mount('#main'); * ``` * * In this way the sandbox will attach itself to the element matching `#main` and will traverse its children * looking for every `.Counter` element attaching an instance of the Counter component onto it. * * To prevent a component for being initialized (for example when you want to initialize it at a later moment) * just add a `data-skip` attribute to its root element. * * @class * @param {object} config * @param {Component[]|[Component, object][]} [config.components] Array of components constructor or array with [ComponentConstructor, options] * @param {HTMLElement|string} [config.root=document.body] Root element of the sandbox. Either a DOM element or a CSS selector * @param {string} [config.id] ID of the sandbox * @property {string} $id Sandbox internal id * @property {HTMLElement} $el Sandbox root DOM element * @property {Context} $ctx Internal [context](/packages/yuzu-application/api/context). Used to share data across child instances * @property {object[]} $registry Registered components storage * @property {Map} $instances Running instances storage * @returns {Sandbox} */ var Sandbox = /*#__PURE__*/ function (_Component) { _inheritsLoose(Sandbox, _Component); /** * Creates a sandbox instance. * * @constructor */ function Sandbox(options) { var _this; if (options === void 0) { options = {}; } _this = _Component.call(this, options) || this; _this.$registry = []; _this.$instances = new Map(); var _this$options = _this.options, _this$options$compone = _this$options.components, components = _this$options$compone === void 0 ? [] : _this$options$compone, id = _this$options.id; _this.$id = id || nextSbUid('_sbx-'); components.forEach(function (config) { if (!Array.isArray(config)) { if (config.root) { _this.register({ component: config, selector: config.root }); } { !config.root && _this.$warn("Skipping component " + (config.displayName || config.name) + " because static \"root\" selector is missing"); } } else { var component = config[0], _config$ = config[1], params = _config$ === void 0 ? {} : _config$; var selector = component.root || params.selector; if (selector) { _this.register(_extends({ component: component, selector: selector }, params)); } { !selector && _this.$warn("Skipping component " + (component.displayName || component.name) + " because a static \"root\" selector is missing and no \"selector\" param is passed-in"); } } }); return _assertThisInitialized(_this) || _assertThisInitialized(_this); } var _proto = Sandbox.prototype; _proto.defaultOptions = function defaultOptions() { return { components: [], context: createContext(), id: '', root: document.body }; } /** * ```js * register(params) * ``` * * Registers a new component into the sandbox. The registered components * will be traversed on `.mount()` initializing every matching component. * * @param {object} params Every property other than `component` and `selector` will be used as component option * @param {Component} params.component Component constructor * @param {string} params.selector Child component root CSS selector * @example * sandbox.register({ * component: Counter, * selector: '.Counter', * theme: 'dark' // <-- instance options * }); */ ; _proto.register = function register(params) { !yuzu.Component.isComponent(params.component) ? invariant(false, 'Missing or invalid `component` property') : void 0; !(typeof params.selector === 'string' || typeof params.selector === 'function') ? invariant(false, 'Missing `selector` property') : void 0; this.$registry.push(params); } /** * ```js * start([data]) * ``` * * **DEPRECATED!** Use `sandbox.mount(root)` instead. * * Starts the sandbox with an optional context. * * The store will be available inside each component at `this.$context`. * * @deprecated * @param {object} [data] Optional context data object to be injected into the child components. * @fires Sandbox#beforeStart * @fires Sandbox#start Events dispatched after all components are initialized * @returns {Sandbox} * @example * sandbox.start(); * * // with context data * sandbox.start({ globalTheme: 'dark' }); */ ; _proto.start = function start(data) { if (data === void 0) { data = {}; } Object.defineProperty(this, '$legacyStart', { value: true }); { this.$warn("Sandbox.start is deprecated. Use the \"mount\" method instead"); } this.mount(this.options.root); this.setup(); this.$ctx && this.$ctx.update(data); this.discover(); return this; } /** * ```js * mount([el], [state]) * ``` * * Enhances `Component.mount()` by firing the child components discovery logic. * By default will use `document.body` as mount element. * * @param {string|Element} el Component's root element * @param {object|null} [state={}] Initial state * @fires Sandbox#beforeStart * @fires Sandbox#start Events dispatched after all components are initialized * @returns {Sandbox} */ ; _proto.mount = function mount(el, state) { if (state === void 0) { state = {}; } _Component.prototype.mount.call(this, el, state); this.$el.setAttribute(Sandbox.SB_DATA_ATTR, ''); if (!this.hasOwnProperty('$legacyStart')) { this.setup(); this.discover(); } return this; } /** * Setups the sandbox context passed in the options. * * @ignore */ ; _proto.setup = function setup() { this.$ctx = this.options.context; this.$ctx.inject(this); } /** * Initializes the sandbox child components. * * @ignore * @returns {Promise} */ ; _proto.discover = function discover() { var _this3 = this; var _this2 = this; !yuzuUtils.isElement(this.$el) ? invariant(false, '"this.$el" is not a DOM element') : void 0; this.emit('beforeStart'); var sbSelector = "[" + Sandbox.SB_DATA_ATTR + "]"; var ret = this.$registry.map(function (_ref) { var ComponentConstructor = _ref.component, selector = _ref.selector, options = _objectWithoutPropertiesLoose(_ref, ["component", "selector"]); try { if (_this2.$instances.has(selector)) { _this2.$warn("Component " + ComponentConstructor + " already initialized on " + selector); return Promise.resolve(); } var targets = _this2.resolveSelector(selector); var instances; if (targets === true) { instances = [_this2.createInstance(ComponentConstructor, options)]; } else if (Array.isArray(targets)) { var $el = _this2.$el; instances = targets.filter(function (el) { return yuzuUtils.isElement(el) && !el.dataset.skip && !el.closest('[data-skip]') && el.closest(sbSelector) === $el; }).map(function (el) { return _this2.createInstance(ComponentConstructor, options, el); }); } var _temp2 = function () { if (instances) { var _this2$$instances2 = _this2.$instances, _set2 = _this2$$instances2.set; return Promise.resolve(Promise.all(instances)).then(function (_Promise$all) { _set2.call(_this2$$instances2, selector, _Promise$all); }); } }(); return Promise.resolve(_temp2 && _temp2.then ? _temp2.then(function () { return true; }) : true); } catch (e) { return Promise.reject(e); } }); return Promise.all(ret).then(function () { _this3.emit('start'); }); } /** * Resolves a configured component selector to a list of DOM nodes or a boolean (for detached components) * * @ignore * @param {string|function} selector Selector string or function. * @returns {HTMLElement[]|boolean} */ ; _proto.resolveSelector = function resolveSelector(selector) { var targets = yuzuUtils.evaluate(selector, this); if (typeof targets === 'string') { targets = this.findNodes(targets); } return targets; } /** * Creates a component instance. * Reads inline components from the passed-in root DOM element. * * @ignore * @param {object} options instance options * @param {HTMLElement} [el] Root element * @returns {Component} */ ; _proto.createInstance = function createInstance(ComponentConstructor, options, el) { var inlineOptions = el ? yuzuUtils.datasetParser(el) : {}; return this.setRef(_extends({ id: nextChildUid(this.$id + '-c.') }, options, {}, inlineOptions, { component: ComponentConstructor, el: el })); } /** * ```js * stop() * ``` * * **DEPRECATED!** Use `sandbox.destroy()` instead. * * Stops every running component, clears sandbox events and destroys the instance. * * @deprecated * @fires Sandbox#beforeStop * @fires Sandbox#stop * @returns {Promise<void>} * @example * sandbox.stop(); */ ; _proto.stop = function stop() { try { var _this5 = this; if ("development" !== 'production') { _this5.$warn("Sandbox.stop is deprecated. Use the \"destroy\" method instead"); } return Promise.resolve(_this5.destroy()); } catch (e) { return Promise.reject(e); } } /** * ```js * destroy() * ``` * * Enhances `Component.destroy()`. * Stops every running component, clears sandbox events and destroys the instance. * * @deprecated * @fires Sandbox#beforeStop * @fires Sandbox#stop * @returns {Promise<void>} * @example * sandbox.destroy(); */ ; _proto.destroy = function destroy() { try { var _this7 = this; _this7.emit('beforeStop'); return Promise.resolve(_this7.beforeDestroy()).then(function () { var _exit = false; function _temp4(_result) { if (_exit) return _result; _this7.$instances.clear(); _this7.emit('stop'); _this7.clear(); return _Component.prototype.destroy.call(_this7); } _this7.removeListeners(); var _temp3 = _catch(function () { if (_this7.$el) { _this7.$el.removeAttribute(Sandbox.SB_DATA_ATTR); } return Promise.resolve(_this7.destroyRefs()).then(function () { _this7.$active = false; }); }, function (e) { _this7.emit('error', e); _exit = true; return Promise.reject(e); }); return _temp3 && _temp3.then ? _temp3.then(_temp4) : _temp4(_temp3); }); } catch (e) { return Promise.reject(e); } } /** * Removes events and associated store * * @ignore */ ; _proto.clear = function clear() { this.$ctx = undefined; // release the context this.off('beforeStart'); this.off('start'); this.off('error'); this.off('beforeStop'); this.off('stop'); }; return Sandbox; }(yuzu.Component); Sandbox.SB_DATA_ATTR = 'data-yuzu-sb'; exports.Sandbox = Sandbox; exports.createContext = createContext; //# sourceMappingURL=yuzu-application.cjs.development.js.map