ng_react.ts 9.9 KB

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