yuzu-application
Version:
Yuzu Application Manager
619 lines (541 loc) • 16.6 kB
JavaScript
'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