Source: component.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. const componentConfigs = {};
  15. let componentPromiseWrappers = {};
  16. const components = {};
  17. let componentsCache = {};
  18. const componentsFn = {};
  19. const DEFAULT_CACHE_VALIDATION_PARAMS = ['p_p_id', 'p_p_lifecycle'];
  20. const DEFAULT_CACHE_VALIDATION_PORTLET_PARAMS = [
  21. 'ddmStructureKey',
  22. 'fileEntryTypeId',
  23. 'folderId',
  24. 'navigation',
  25. 'status',
  26. ];
  27. const LIFERAY_COMPONENT = 'liferay.component';
  28. const _createPromiseWrapper = function (value) {
  29. let promiseWrapper;
  30. if (value) {
  31. promiseWrapper = {
  32. promise: Promise.resolve(value),
  33. resolve() {},
  34. };
  35. }
  36. else {
  37. let promiseResolve;
  38. const promise = new Promise((resolve) => {
  39. promiseResolve = resolve;
  40. });
  41. promiseWrapper = {
  42. promise,
  43. resolve: promiseResolve,
  44. };
  45. }
  46. return promiseWrapper;
  47. };
  48. /**
  49. * Restores a previously cached component markup.
  50. *
  51. * @param {object} state The stored state associated with the registered task.
  52. * @param {object} params The additional params passed in the task registration.
  53. * @param {Fragment} node The temporary fragment holding the new markup.
  54. * @private
  55. */
  56. const _restoreTask = function (state, params, node) {
  57. const cache = state.data;
  58. const componentIds = Object.keys(cache);
  59. componentIds.forEach((componentId) => {
  60. const container = node.querySelector(`#${componentId}`);
  61. if (container) {
  62. container.innerHTML = cache[componentId].html;
  63. }
  64. });
  65. };
  66. /**
  67. * Runs when an SPA navigation start is detected to
  68. *
  69. * <ul>
  70. * <li>
  71. * Cache the state and current markup of registered components that have
  72. * requested it through the <code>cacheState</code> configuration option. This
  73. * state can be used to initialize the component in the same state if it
  74. * persists throughout navigations.
  75. * </li>
  76. * <li>
  77. * Register a DOM task to restore the markup of components that are present in
  78. * the next screen to avoid a flickering effect due to state changes. This can
  79. * be done by querying the components screen cache using the
  80. * <code>Liferay.getComponentsCache</code> method.
  81. * </li>
  82. * </ul>
  83. *
  84. * @private
  85. */
  86. const _onStartNavigate = function (event) {
  87. const currentUri = new URL(window.location.href);
  88. const uri = new URL(event.path, window.location.href);
  89. const cacheableUri = DEFAULT_CACHE_VALIDATION_PARAMS.every((param) => {
  90. return (
  91. uri.searchParams.get(param) === currentUri.searchParams.get(param)
  92. );
  93. });
  94. if (cacheableUri) {
  95. var componentIds = Object.keys(components);
  96. componentIds = componentIds.filter((componentId) => {
  97. const component = components[componentId];
  98. if (!component) {
  99. return false;
  100. }
  101. const componentConfig = componentConfigs[componentId];
  102. const cacheablePortletUri = DEFAULT_CACHE_VALIDATION_PORTLET_PARAMS.every(
  103. (param) => {
  104. let cacheable = false;
  105. if (componentConfig) {
  106. const namespacedParam = `_${componentConfig.portletId}_${param}`;
  107. cacheable =
  108. uri.searchParams.get(namespacedParam) ===
  109. currentUri.searchParams.get(namespacedParam);
  110. }
  111. return cacheable;
  112. }
  113. );
  114. const cacheableComponent =
  115. typeof component.isCacheable === 'function'
  116. ? component.isCacheable(uri)
  117. : false;
  118. return (
  119. cacheableComponent &&
  120. cacheablePortletUri &&
  121. componentConfig &&
  122. componentConfig.cacheState &&
  123. component.element &&
  124. component.getState
  125. );
  126. });
  127. componentsCache = componentIds.reduce((cache, componentId) => {
  128. const component = components[componentId];
  129. const componentConfig = componentConfigs[componentId];
  130. const componentState = component.getState();
  131. const componentCache = componentConfig.cacheState.reduce(
  132. (cache, stateKey) => {
  133. cache[stateKey] = componentState[stateKey];
  134. return cache;
  135. },
  136. {}
  137. );
  138. cache[componentId] = {
  139. html: component.element.innerHTML,
  140. state: componentCache,
  141. };
  142. return cache;
  143. }, []);
  144. Liferay.DOMTaskRunner.addTask({
  145. action: _restoreTask,
  146. condition: (state) => state.owner === LIFERAY_COMPONENT,
  147. });
  148. Liferay.DOMTaskRunner.addTaskState({
  149. data: componentsCache,
  150. owner: LIFERAY_COMPONENT,
  151. });
  152. }
  153. else {
  154. componentsCache = {};
  155. }
  156. };
  157. /**
  158. * Registers a component and retrieves its instance from the global registry.
  159. *
  160. * @param {string} id The ID of the component to retrieve or register.
  161. * @param {object} value The component instance or a component constructor. If
  162. * a constructor is provided, it will be invoked the first time the
  163. * component is requested and its result will be stored and returned as
  164. * the component.
  165. * @param {object} componentConfig The Custom component configuration. This can
  166. * be used to provide additional hints for the system handling of the
  167. * component lifecycle.
  168. * @return {object} The passed value, or the stored component for the provided
  169. * ID.
  170. */
  171. const component = function (id, value, componentConfig) {
  172. let retVal;
  173. if (arguments.length === 1) {
  174. let component = components[id];
  175. if (component && typeof component === 'function') {
  176. componentsFn[id] = component;
  177. component = component();
  178. components[id] = component;
  179. }
  180. retVal = component;
  181. }
  182. else {
  183. if (components[id] && value !== null) {
  184. delete componentConfigs[id];
  185. delete componentPromiseWrappers[id];
  186. console.warn(
  187. 'Component with id "' +
  188. id +
  189. '" is being registered twice. This can lead to unexpected behaviour in the "Liferay.component" and "Liferay.componentReady" APIs, as well as in the "*:registered" events.'
  190. );
  191. }
  192. retVal = components[id] = value;
  193. if (value === null) {
  194. delete componentConfigs[id];
  195. delete componentPromiseWrappers[id];
  196. }
  197. else {
  198. componentConfigs[id] = componentConfig;
  199. Liferay.fire(id + ':registered');
  200. const componentPromiseWrapper = componentPromiseWrappers[id];
  201. if (componentPromiseWrapper) {
  202. componentPromiseWrapper.resolve(value);
  203. }
  204. else {
  205. componentPromiseWrappers[id] = _createPromiseWrapper(value);
  206. }
  207. }
  208. }
  209. return retVal;
  210. };
  211. /**
  212. * Retrieves a list of component instances after they've been registered.
  213. *
  214. * @param {...string} componentId The IDs of the components to receive.
  215. * @return {Promise} A promise to be resolved with all the requested component
  216. * instances after they've been successfully registered.
  217. */
  218. const componentReady = function () {
  219. let component;
  220. let componentPromise;
  221. if (arguments.length === 1) {
  222. component = arguments[0];
  223. }
  224. else {
  225. component = [];
  226. for (var i = 0; i < arguments.length; i++) {
  227. component[i] = arguments[i];
  228. }
  229. }
  230. if (Array.isArray(component)) {
  231. componentPromise = Promise.all(
  232. component.map((id) => componentReady(id))
  233. );
  234. }
  235. else {
  236. let componentPromiseWrapper = componentPromiseWrappers[component];
  237. if (!componentPromiseWrapper) {
  238. componentPromiseWrappers[
  239. component
  240. ] = componentPromiseWrapper = _createPromiseWrapper();
  241. }
  242. componentPromise = componentPromiseWrapper.promise;
  243. }
  244. return componentPromise;
  245. };
  246. /**
  247. * Destroys the component registered by the provided component ID. This invokes
  248. * the component's own destroy lifecycle methods (destroy or dispose) and
  249. * deletes the internal references to the component in the component registry.
  250. *
  251. * @param {string} componentId The ID of the component to destroy.
  252. */
  253. const destroyComponent = function (componentId) {
  254. const component = components[componentId];
  255. if (component) {
  256. const destroyFn = component.destroy || component.dispose;
  257. if (destroyFn) {
  258. destroyFn.call(component);
  259. }
  260. delete componentConfigs[componentId];
  261. delete componentPromiseWrappers[componentId];
  262. delete componentsFn[componentId];
  263. delete components[componentId];
  264. }
  265. };
  266. /**
  267. * Destroys registered components matching the provided filter function. If no
  268. * filter function is provided, it destroys all registered components.
  269. *
  270. * @param {Function} filterFn A method that receives a component's destroy
  271. * options and the component itself, and returns <code>true</code> if the
  272. * component should be destroyed.
  273. */
  274. const destroyComponents = function (filterFn) {
  275. var componentIds = Object.keys(components);
  276. if (filterFn) {
  277. componentIds = componentIds.filter((componentId) => {
  278. return filterFn(
  279. components[componentId],
  280. componentConfigs[componentId] || {}
  281. );
  282. });
  283. }
  284. componentIds.forEach(destroyComponent);
  285. };
  286. /**
  287. * Clears the component promises map to make sure pending promises don't get
  288. * accidentally resolved at a later stage if a component with the same ID
  289. * appears, causing stale code to run.
  290. */
  291. const destroyUnfulfilledPromises = function () {
  292. componentPromiseWrappers = {};
  293. };
  294. /**
  295. * Retrieves a registered component's cached state.
  296. *
  297. * @param {string} componentId The ID used to register the component.
  298. * @return {object} The state the component had prior to the previous navigation.
  299. */
  300. const getComponentCache = function (componentId) {
  301. const componentCache = componentsCache[componentId];
  302. return componentCache ? componentCache.state : {};
  303. };
  304. /**
  305. * Initializes the component cache mechanism.
  306. */
  307. const initComponentCache = function () {
  308. Liferay.on('startNavigate', _onStartNavigate);
  309. };
  310. export {
  311. component,
  312. componentReady,
  313. destroyComponent,
  314. destroyComponents,
  315. destroyUnfulfilledPromises,
  316. getComponentCache,
  317. initComponentCache,
  318. };
  319. export default component;