Source: data-engine-js-components-web/src/main/resources/META-INF/resources/js/utils/ReactComponentAdapter.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 IncrementalDomRenderer from 'metal-incremental-dom';
  15. import JSXComponent from 'metal-jsx';
  16. import {Config} from 'metal-state';
  17. import React, {useEffect, useState} from 'react';
  18. import ReactDOM from 'react-dom';
  19. import Observer from './Observer.es';
  20. const CONFIG_BLACKLIST = ['children', 'events', 'ref', 'visible'];
  21. const CONFIG_DEFAULT = ['displayErrors'];
  22. /**
  23. * The Adapter creates a communication bridge between the Metal and React components.
  24. * The Adapter when it is rendered for the first time uses `ReactDOM.render` to assemble
  25. * the component and subsequent renderings are done by React. We created a tunnel with
  26. * an Observer that updates the internal state of the component in React that makes a
  27. * wrapper over the main component to force React to render at the best time, we also
  28. * ignore the rendering of Metal.
  29. *
  30. * @example
  31. * // import getConnectedReactComponentAdapter from '/path/ReactComponentAdapter.es';
  32. * //
  33. * // const ReactComponent = ({children, className}) => <div className={className}>{children}</div>;
  34. * // const ReactComponentAdapter = getConnectedReactComponentAdapter(
  35. * // ReactComponent
  36. * // );
  37. * //
  38. * // In the rendering of Metal
  39. * // render() {
  40. * // return (
  41. * // <ReactComponentAdapter className="h1-title">
  42. * // <h1>{'Title'}</h1>
  43. * // </ReactComponentAdapter>
  44. * // );
  45. * // }
  46. *
  47. * To call the React component in the context of Metal + soy, where varient is not an option,
  48. * you can use Metal's `Soy.register` to create a fake component so that you can call the React
  49. * component in Soy. The use of children from Soy components for React does not work.
  50. *
  51. * @example
  52. * // import Soy from 'metal-soy';
  53. * // import getConnectedReactComponentAdapter from '/path/ReactComponentAdapter.es';
  54. * // import templates from './FakeAdapter.soy';
  55. * //
  56. * // const ReactComponent = ({className}) => <div className={className} />;
  57. * // const ReactComponentAdapter = getConnectedReactComponentAdapter(
  58. * // ReactComponent
  59. * // );
  60. * // Soy.register(ReactComponentAdapter, templates);
  61. * //
  62. * // In soy
  63. * // {call FakeAdapter.render}
  64. * // {param className: 'test' /}
  65. * // {/call}
  66. *
  67. * @param {React.createElement} ReactComponent
  68. */
  69. function getConnectedReactComponentAdapter(ReactComponent) {
  70. class ReactComponentAdapter extends JSXComponent {
  71. /**
  72. * For Metal to track config changes, we need to config the state so
  73. * that willReceiveProps is called as expected.
  74. */
  75. constructor(config, parentElement) {
  76. const props = {};
  77. Object.keys(config)
  78. .concat(CONFIG_DEFAULT)
  79. .forEach((key) => {
  80. if (!CONFIG_BLACKLIST.includes(key)) {
  81. props[key] = Config.any().value(config[key]);
  82. }
  83. });
  84. super(config, parentElement);
  85. const data = JSXComponent.DATA_MANAGER.getManagerData(this);
  86. data.props_.configState({
  87. ...JSXComponent.DATA,
  88. ...props,
  89. });
  90. }
  91. created() {
  92. this.observer = new Observer();
  93. this.reactComponentRef = React.createRef();
  94. }
  95. disposed() {
  96. if (this.instance_) {
  97. ReactDOM.unmountComponentAtNode(this.instance_);
  98. this.instance_ = null;
  99. }
  100. }
  101. willReceiveProps(changes) {
  102. // Delete the events and children properties to make it easier to
  103. // check which values have been changed, events and children are
  104. // properties that are changing all the time when new renderings
  105. // happen, a new reference is created all the time.
  106. delete changes.events;
  107. delete changes.children;
  108. if (changes && Object.keys(changes).length > 0) {
  109. const newValues = {};
  110. const keys = Object.keys(changes);
  111. keys.forEach((key) => {
  112. if (!CONFIG_BLACKLIST.includes(key)) {
  113. newValues[key] = changes[key].newVal;
  114. }
  115. });
  116. this.observer.dispatch(newValues);
  117. }
  118. }
  119. /**
  120. * Disable Metal rendering and let React render in the best
  121. * possible way.
  122. */
  123. shouldUpdate() {
  124. return false;
  125. }
  126. syncVisible(value) {
  127. this.observer.dispatch({visible: value});
  128. }
  129. render() {
  130. const {events, ref, store, ...otherProps} = this.props;
  131. /* eslint-disable no-undef */
  132. IncrementalDOM.elementOpen(
  133. 'div',
  134. ref,
  135. [],
  136. 'class',
  137. 'react-component-adapter'
  138. );
  139. const element = IncrementalDOM.currentElement();
  140. IncrementalDOM.skip();
  141. IncrementalDOM.elementClose('div');
  142. /* eslint-enable no-undef */
  143. // eslint-disable-next-line @liferay/portal/no-react-dom-render
  144. ReactDOM.render(
  145. <ObserverSubscribe observer={this.observer}>
  146. <ReactComponent
  147. {...otherProps}
  148. {...events}
  149. {...store}
  150. instance={this}
  151. ref={this.reactComponentRef}
  152. />
  153. </ObserverSubscribe>,
  154. element
  155. );
  156. this.instance_ = element;
  157. }
  158. }
  159. ReactComponentAdapter.RENDERER = IncrementalDomRenderer;
  160. return ReactComponentAdapter;
  161. }
  162. /**
  163. * Adds a sub observer to maintain the updated state of the
  164. * component.
  165. */
  166. const ObserverSubscribe = ({children, observer}) => {
  167. const [state, setState] = useState({});
  168. useEffect(() => {
  169. const change = (value) => setState({...state, ...value});
  170. observer.subscribe(change);
  171. return () => {
  172. observer.unsubscribe(change);
  173. };
  174. }, [state, setState, observer]);
  175. return React.cloneElement(children, {
  176. ...children.props,
  177. ...state,
  178. });
  179. };
  180. export {getConnectedReactComponentAdapter};
  181. export default getConnectedReactComponentAdapter;