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