ng_react.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. //
  2. // This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199
  3. //
  4. // # ngReact
  5. // ### Use React Components inside of your Angular applications
  6. //
  7. // Composed of
  8. // - reactComponent (generic directive for delegating off to React Components)
  9. // - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
  10. import angular, { auto } from 'angular';
  11. import { kebabCase } from 'lodash';
  12. import React, { ComponentType } from 'react';
  13. import ReactDOM from 'react-dom';
  14. // get a react component from name (components can be an angular injectable e.g. value, factory or
  15. // available on window
  16. function getReactComponent(name: string | Function, $injector: auto.IInjectorService): ComponentType {
  17. // if name is a function assume it is component and return it
  18. if (angular.isFunction(name)) {
  19. return name as unknown as ComponentType;
  20. }
  21. // a React component name must be specified
  22. if (!name) {
  23. throw new Error('ReactComponent name attribute must be specified');
  24. }
  25. // ensure the specified React component is accessible, and fail fast if it's not
  26. let reactComponent;
  27. try {
  28. reactComponent = $injector.get(name);
  29. } catch (e) {}
  30. if (!reactComponent) {
  31. try {
  32. reactComponent = name.split('.').reduce((current, namePart) => {
  33. // @ts-ignore
  34. return current[namePart];
  35. }, window);
  36. } catch (e) {}
  37. }
  38. if (!reactComponent) {
  39. throw Error('Cannot find react component ' + name);
  40. }
  41. return reactComponent as unknown as ComponentType;
  42. }
  43. // wraps a function with scope.$apply, if already applied just return
  44. function applied(fn: any, scope: any) {
  45. if (fn.wrappedInApply) {
  46. return fn;
  47. }
  48. // this had the equivalent of `eslint-disable-next-line prefer-arrow/prefer-arrow-functions`
  49. const wrapped: any = function () {
  50. const args = arguments;
  51. const phase = scope.$root.$$phase;
  52. if (phase === '$apply' || phase === '$digest') {
  53. return fn.apply(null, args);
  54. } else {
  55. return scope.$apply(() => {
  56. return fn.apply(null, args);
  57. });
  58. }
  59. };
  60. wrapped.wrappedInApply = true;
  61. return wrapped;
  62. }
  63. /**
  64. * wraps functions on obj in scope.$apply
  65. *
  66. * keeps backwards compatibility, as if propsConfig is not passed, it will
  67. * work as before, wrapping all functions and won't wrap only when specified.
  68. *
  69. * @version 0.4.1
  70. * @param obj react component props
  71. * @param scope current scope
  72. * @param propsConfig configuration object for all properties
  73. * @returns {Object} props with the functions wrapped in scope.$apply
  74. */
  75. function applyFunctions(obj: any, scope: any, propsConfig?: any): object {
  76. return Object.keys(obj || {}).reduce((prev, key) => {
  77. const value = obj[key];
  78. const config = (propsConfig || {})[key] || {};
  79. /**
  80. * wrap functions in a function that ensures they are scope.$applied
  81. * ensures that when function is called from a React component
  82. * the Angular digest cycle is run
  83. */
  84. // @ts-ignore
  85. prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value;
  86. return prev;
  87. }, {});
  88. }
  89. /**
  90. *
  91. * @param watchDepth (value of HTML watch-depth attribute)
  92. * @param scope (angular scope)
  93. *
  94. * Uses the watchDepth attribute to determine how to watch props on scope.
  95. * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value
  96. */
  97. function watchProps(watchDepth: string, scope: any, watchExpressions: any[], listener: any) {
  98. const supportsWatchCollection = angular.isFunction(scope.$watchCollection);
  99. const supportsWatchGroup = angular.isFunction(scope.$watchGroup);
  100. const watchGroupExpressions = [];
  101. for (const expr of watchExpressions) {
  102. const actualExpr = getPropExpression(expr);
  103. const exprWatchDepth = getPropWatchDepth(watchDepth, expr);
  104. // ignore empty expressions & expressions with functions
  105. if (!actualExpr || actualExpr.match(/\(.*\)/) || exprWatchDepth === 'one-time') {
  106. continue;
  107. }
  108. if (exprWatchDepth === 'collection' && supportsWatchCollection) {
  109. scope.$watchCollection(actualExpr, listener);
  110. } else if (exprWatchDepth === 'reference' && supportsWatchGroup) {
  111. watchGroupExpressions.push(actualExpr);
  112. } else {
  113. scope.$watch(actualExpr, listener, exprWatchDepth !== 'reference');
  114. }
  115. }
  116. if (watchDepth === 'one-time') {
  117. listener();
  118. }
  119. if (watchGroupExpressions.length) {
  120. scope.$watchGroup(watchGroupExpressions, listener);
  121. }
  122. }
  123. // render React component, with scope[attrs.props] being passed in as the component props
  124. function renderComponent(component: any, props: object, scope: any, elem: Element[]) {
  125. scope.$evalAsync(() => {
  126. ReactDOM.render(React.createElement(component, props), elem[0]);
  127. });
  128. }
  129. // get prop name from prop (string or array)
  130. function getPropName(prop: any) {
  131. return Array.isArray(prop) ? prop[0] : prop;
  132. }
  133. // get prop name from prop (string or array)
  134. function getPropConfig(prop: any) {
  135. return Array.isArray(prop) ? prop[1] : {};
  136. }
  137. // get prop expression from prop (string or array)
  138. function getPropExpression(prop: any) {
  139. return Array.isArray(prop) ? prop[0] : prop;
  140. }
  141. /**
  142. * Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
  143. * kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
  144. * evaluated by the browser as such.
  145. * @param attrs All attributes of the component.
  146. * @param propName Name of the prop that react component expects.
  147. */
  148. function findAttribute(attrs: object, propName: string): string {
  149. const index = Object.keys(attrs).find((attr: any) => {
  150. return attr.toLowerCase() === propName.toLowerCase() || kebabCase(attr) === kebabCase(propName);
  151. });
  152. // @ts-ignore
  153. return attrs[index];
  154. }
  155. // get watch depth of prop (string or array)
  156. function getPropWatchDepth(defaultWatch: string, prop: string | any[]) {
  157. const customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth;
  158. return customWatchDepth || defaultWatch;
  159. }
  160. // # reactComponent
  161. // Directive that allows React components to be used in Angular templates.
  162. //
  163. // Usage:
  164. // <react-component name="Hello" props="name"/>
  165. //
  166. // This requires that there exists an injectable or globally available 'Hello' React component.
  167. // The 'props' attribute is optional and is passed to the component.
  168. //
  169. // The following would would create and register the component:
  170. //
  171. // var module = angular.module('ace.react.components');
  172. // module.value('Hello', React.createClass({
  173. // render: function() {
  174. // return <div>Hello {this.props.name}</div>;
  175. // }
  176. // }));
  177. //
  178. const reactComponent = ($injector: any): any => {
  179. return {
  180. restrict: 'E',
  181. replace: true,
  182. link: function (scope: any, elem: Element[], attrs: any) {
  183. const reactComponent = getReactComponent(attrs.name, $injector);
  184. const renderMyComponent = () => {
  185. const scopeProps = scope.$eval(attrs.props);
  186. const props = applyFunctions(scopeProps, scope);
  187. renderComponent(reactComponent, props, scope, elem);
  188. };
  189. // If there are props, re-render when they change
  190. attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent();
  191. // cleanup when scope is destroyed
  192. scope.$on('$destroy', () => {
  193. if (!attrs.onScopeDestroy) {
  194. ReactDOM.unmountComponentAtNode(elem[0]);
  195. } else {
  196. scope.$eval(attrs.onScopeDestroy, {
  197. unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
  198. });
  199. }
  200. });
  201. },
  202. };
  203. };
  204. // # reactDirective
  205. // Factory function to create directives for React components.
  206. //
  207. // With a component like this:
  208. //
  209. // var module = angular.module('ace.react.components');
  210. // module.value('Hello', React.createClass({
  211. // render: function() {
  212. // return <div>Hello {this.props.name}</div>;
  213. // }
  214. // }));
  215. //
  216. // A directive can be created and registered with:
  217. //
  218. // module.directive('hello', function(reactDirective) {
  219. // return reactDirective('Hello', ['name']);
  220. // });
  221. //
  222. // Where the first argument is the injectable or globally accessible name of the React component
  223. // and the second argument is an array of property names to be watched and passed to the React component
  224. // as props.
  225. //
  226. // This directive can then be used like this:
  227. //
  228. // <hello name="name"/>
  229. //
  230. const reactDirective = ($injector: auto.IInjectorService) => {
  231. return (reactComponentName: string, props: string[], conf: any, injectableProps: any) => {
  232. const directive = {
  233. restrict: 'E',
  234. replace: true,
  235. link: function (scope: any, elem: Element[], attrs: any) {
  236. const reactComponent = getReactComponent(reactComponentName, $injector);
  237. // if props is not defined, fall back to use the React component's propTypes if present
  238. props = props || Object.keys(reactComponent.propTypes || {});
  239. // for each of the properties, get their scope value and set it to scope.props
  240. const renderMyComponent = () => {
  241. let scopeProps: any = {};
  242. const config: any = {};
  243. props.forEach((prop) => {
  244. const propName = getPropName(prop);
  245. scopeProps[propName] = scope.$eval(findAttribute(attrs, propName));
  246. config[propName] = getPropConfig(prop);
  247. });
  248. scopeProps = applyFunctions(scopeProps, scope, config);
  249. scopeProps = angular.extend({}, scopeProps, injectableProps);
  250. renderComponent(reactComponent, scopeProps, scope, elem);
  251. };
  252. // watch each property name and trigger an update whenever something changes,
  253. // to update scope.props with new values
  254. const propExpressions = props.map((prop) => {
  255. return Array.isArray(prop)
  256. ? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
  257. : findAttribute(attrs, prop);
  258. });
  259. // If we don't have any props, then our watch statement won't fire.
  260. props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent();
  261. // cleanup when scope is destroyed
  262. scope.$on('$destroy', () => {
  263. if (!attrs.onScopeDestroy) {
  264. ReactDOM.unmountComponentAtNode(elem[0]);
  265. } else {
  266. scope.$eval(attrs.onScopeDestroy, {
  267. unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
  268. });
  269. }
  270. });
  271. },
  272. };
  273. return angular.extend(directive, conf);
  274. };
  275. };
  276. const ngModule = angular.module('react', []);
  277. ngModule.directive('reactComponent', ['$injector', reactComponent]);
  278. ngModule.factory('reactDirective', ['$injector', reactDirective]);