@empathyco/x-components
Version:
Empathy X Components
325 lines (322 loc) • 11.7 kB
JavaScript
import { deepMerge } from '@empathyco/x-deep-merge';
import { forEach } from '@empathyco/x-utils';
import { createStore } from 'vuex';
import { cleanGettersProxyCache } from '../store/utils/getters-proxy.utils.js';
import { RootXStoreModule } from '../store/x.module.js';
import { sendWiringToDevtools } from './devtools/wiring.devtools.js';
import { bus } from './x-bus.js';
import { registerStoreEmitters } from './x-emitters.js';
import { assertXPluginOptionsAreValid } from './x-plugin.utils.js';
/**
* Vue plugin that initializes the properties needed by the x-components, and exposes the events bus
* and the adapter after it has been installed.
*
* @public
*/
class XPlugin {
/**
* XComponentsAdapter Is the middleware between the components and our API where data can be
* mapped to client needs.
* This property is only available after installing the plugin.
*
* @returns The installed adapter.
* @throws If this property is accessed before calling `Vue.use(xPlugin)`.
* @public
*/
static get adapter() {
return this.getInstance().adapter;
}
/**
* Exposed XBus, so any kind of application can subscribe to
* {@link XEventsTypes} without having to pass through a component.
* This property is only available after installing the plugin.
*
* @returns The installed bus.
* @throws If this property is accessed before calling `Vue.use(xPlugin)`.
* @public
*/
static get bus() {
return this.getInstance().bus;
}
/**
* {@link https://vuex.vuejs.org | Vuex Store} Is the place where all shared data
* is saved.
*
* @returns The installed store.
* @throws If this property is accessed before calling `Vue.use(xPlugin)`.
* @public
*/
static get store() {
return this.getInstance().store;
}
/**
* Safely retrieves the installed instance of the XPlugin.
*
* @returns The installed instance of the XPlugin.
* @throws If this method is called before calling `Vue.use(xPlugin)`.
* @internal
*/
static getInstance() {
if (!this.instance) {
throw new Error("XPlugin must be installed before accessing it's API.");
}
return this.instance;
}
/**
* Creates a new instance of the XPlugin with the given bus passed as parameter.
*
* @param bus - The XBus implementation to use for the plugin.
*
* @public
*/
constructor(bus) {
this.wiring = {};
/**
* Set of the already installed XModules to avoid re-registering them.
*
* @internal
*/
this.installedXModules = new Set();
/**
* True if the plugin has been installed in a Vue instance, in this case
* XModules will be installed immediately. False otherwise, in this case
* XModules will be installed lazily when the {@link XPlugin#install} method
* is called.
*
* @internal
*/
this.isInstalled = false;
this.bus = bus;
}
/**
* If the plugin has already been installed, it immediately registers a {@link XModule}. If it
* has not been installed yet, it stores the module in a list until the plugin is installed.
*
* @param xModule - The module to register.
*
* @public
*/
static registerXModule(xModule) {
if (this.instance) {
this.instance.registerXModule(xModule);
}
else {
this.lazyRegisterXModule(xModule);
}
}
/**
* Utility method for resetting the installed instance of the plugin.
*
* @remarks Use only for testing.
*
* @internal
*/
static resetInstance() {
cleanGettersProxyCache();
this.instance = undefined;
}
/**
* Stores the {@link XModule} in a dictionary, so it can be registered later in the installation
* process.
*
* @param xModule - The module to register.
*
* @internal
*/
static lazyRegisterXModule(xModule) {
this.pendingXModules[xModule.name] = xModule;
}
/**
* Installs the plugin into the Vue instance.
*
* @param app - The Vue application instance.
* @param options - The options to install this plugin with.
* @throws If the XPlugin has already been installed, or the options are not valid.
*
* @internal
*/
install(app, options) {
if (this.isInstalled) {
throw new Error('XPlugin has already been installed');
}
assertXPluginOptionsAreValid(options);
XPlugin.instance = this;
this.app = app;
this.options = options;
this.adapter = options.adapter;
this.registerStore();
this.registerInitialModules();
this.registerPendingXModules();
this.isInstalled = true;
}
/**
* Performs the registration of a {@link XModule}.
*
* @param xModule - The module to register.
*
* @internal
*/
registerXModule(xModule) {
if (!this.installedXModules.has(xModule.name)) {
const customizedXModule = this.customizeXModule(xModule);
this.registerStoreModule(customizedXModule);
this.registerStoreEmitters(customizedXModule);
this.registerWiring(customizedXModule);
// The wiring must be registered after the store emitters
// to allow lazy loaded modules work properly.
this.installedXModules.add(xModule.name);
void this.bus.emit('ModuleRegistered', xModule.name);
}
}
/**
* Performs a customization of a {@link XModule} using the XPlugin public and private options.
*
* @param xModule - The module to customize.
* @returns The customized xModule.
* @internal
*/
customizeXModule({ name, wiring, storeModule, storeEmitters, ...restXModule }) {
const { wiring: wiringOptions, config } = this.options.xModules?.[name] ?? {};
const { storeModule: storeModuleOptions, storeEmitters: emittersOptions } = this.options.__PRIVATE__xModules?.[name] ?? {};
return {
name,
// eslint-disable-next-line ts/no-unsafe-assignment
wiring: wiringOptions ? deepMerge({}, wiring, wiringOptions) : wiring,
storeModule: this.customizeStoreModule(storeModule, storeModuleOptions ?? {}, config),
// eslint-disable-next-line ts/no-unsafe-assignment
storeEmitters: emittersOptions
? deepMerge({}, storeEmitters, emittersOptions)
: storeEmitters,
...restXModule,
};
}
/**
* Performs the registration of the wiring, retrieving the observable for each event, and
* executing each wire.
*
* @param xModule - The {@link XModule} to register its wiring.
* @internal
*/
registerWiring({ wiring, name }) {
sendWiringToDevtools(name, wiring);
forEach(wiring, (event, wires) => {
// Obtain the observable
const observable = this.bus.on(event, true);
// Register event wires
forEach(wires, (_, wire) => {
wire(observable, this.store, this.bus.on.bind(this.bus));
});
});
}
/**
* Registers a {@link https://vuex.vuejs.org/ | Vuex} store module under the 'x' module.
*
* @param xModule - The {@link XModule} to register its Store Module.
* @internal
*/
registerStoreModule({ name, storeModule }) {
storeModule.namespaced = true;
this.store.registerModule(['x', name], storeModule);
}
/**
* Overrides a {@link https://vuex.vuejs.org/ | Vuex} store module definition.
*
* Priority of configuration merging.
* 1st {@link XPluginOptions.xModules | xModules XPlugin option}.
* 2nd {@link XPluginOptions.__PRIVATE__xModules | Private xModules XPlugin option}.
* 3rd {@link XStoreModule.state | Default state of the xModule}.
*
* @param defaultModule - The default store module to override.
* @param moduleOptions - The state, actions, mutations and getters to override the defaultModule.
* @param configOptions - The state config to override the moduleOptions.
* @returns The {@link XStoreModule} customized.
* @internal
*/
customizeStoreModule({ state: defaultState, ...actionsGettersMutations }, { state: xModuleState, ...newActionsGettersMutations }, configOptions) {
const configOptionsObject = configOptions ? { config: configOptions } : {};
// eslint-disable-next-line ts/no-unsafe-assignment
const customizedModule = deepMerge({}, actionsGettersMutations, newActionsGettersMutations);
// eslint-disable-next-line ts/no-unsafe-assignment,ts/no-unsafe-member-access
customizedModule.state = deepMerge(defaultState(), xModuleState, configOptionsObject);
// eslint-disable-next-line ts/no-unsafe-return
return customizedModule;
}
/**
* Registers the store emitters, making them emit the event when the part of the state selected
* changes.
*
* @param xModule - The {@link XModule} to register its Store Emitters.
* @internal
*/
registerStoreEmitters(xModule) {
registerStoreEmitters(xModule, this.bus, this.store);
}
/**
* Registers the {@link https://vuex.vuejs.org/ | Vuex} store. If the store has not been passed
* through the {@link XPluginOptions} object, it creates one, and injects it in the Vue
* prototype. Then it registers an x module in the store, to safe scope all the
* {@link XModule | XModules} dynamically installed.
*
* @internal
*/
registerStore() {
this.store = this.options.store ?? createStore({});
this.app.use(this.store);
this.store.registerModule('x', RootXStoreModule);
}
/**
* Registers the initial {@link XModule | XModules} during the {@link XPlugin} installation.
*
* @internal
*/
registerInitialModules() {
this.options.initialXModules?.forEach(xModule => {
this.registerXModule(xModule);
});
}
/**
* Registers the pending {@link XModule | XModules}, that requested to be registered before the
* installation of the plugin.
*
* @internal
*/
registerPendingXModules() {
forEach(XPlugin.pendingXModules, (_, xModule) => {
this.registerXModule(xModule);
});
XPlugin.pendingXModules = {};
}
}
/**
* Record of modules that have been tried to be installed before the installation of the plugin.
*
* @internal
*/
XPlugin.pendingXModules = {};
/**
* Vue plugin that modifies each component instance, extending them with the
* {@link XComponentAPI | X Component API }.
*
* @example
* Minimal installation example. An API adapter is needed to connect the X Components with the
* suggestions, search, or tagging APIs. In this example we are using the default Empathy's platform
* adapter.
*
* ```typescript
* import { platformAdapter } from '@empathyco/x-adapter-platform';
* Vue.use(xPlugin, { adapter: platformAdapter });
* ```
*
* @example
* If you are using {@link https://vuex.vuejs.org/ | Vuex} in your project you must install its
* plugin, and instantiate a store before installing the XPlugin:
* ```typescript
* Vue.use(Vuex);
* const store = new Store({ ... });
* Vue.use(xPlugin, { adapter, store });
* ```
* @public
*/
const xPlugin = new XPlugin(bus);
export { XPlugin, xPlugin };
//# sourceMappingURL=x-plugin.js.map