| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- //
- // This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199
- //
- // # ngReact
- // ### Use React Components inside of your Angular applications
- //
- // Composed of
- // - reactComponent (generic directive for delegating off to React Components)
- // - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
- import React from 'react';
- import ReactDOM from 'react-dom';
- import angular from 'angular';
- // get a react component from name (components can be an angular injectable e.g. value, factory or
- // available on window
- function getReactComponent(name, $injector) {
- // if name is a function assume it is component and return it
- if (angular.isFunction(name)) {
- return name;
- }
- // a React component name must be specified
- if (!name) {
- throw new Error('ReactComponent name attribute must be specified');
- }
- // ensure the specified React component is accessible, and fail fast if it's not
- let reactComponent;
- try {
- reactComponent = $injector.get(name);
- } catch (e) {}
- if (!reactComponent) {
- try {
- reactComponent = name.split('.').reduce((current, namePart) => {
- return current[namePart];
- }, window);
- } catch (e) {}
- }
- if (!reactComponent) {
- throw Error('Cannot find react component ' + name);
- }
- return reactComponent;
- }
- // wraps a function with scope.$apply, if already applied just return
- function applied(fn, scope) {
- if (fn.wrappedInApply) {
- return fn;
- }
- //tslint:disable-next-line:only-arrow-functions
- const wrapped: any = function() {
- const args = arguments;
- const phase = scope.$root.$$phase;
- if (phase === '$apply' || phase === '$digest') {
- return fn.apply(null, args);
- } else {
- return scope.$apply(() => {
- return fn.apply(null, args);
- });
- }
- };
- wrapped.wrappedInApply = true;
- return wrapped;
- }
- /**
- * wraps functions on obj in scope.$apply
- *
- * keeps backwards compatibility, as if propsConfig is not passed, it will
- * work as before, wrapping all functions and won't wrap only when specified.
- *
- * @version 0.4.1
- * @param obj react component props
- * @param scope current scope
- * @param propsConfig configuration object for all properties
- * @returns {Object} props with the functions wrapped in scope.$apply
- */
- function applyFunctions(obj, scope, propsConfig?) {
- return Object.keys(obj || {}).reduce((prev, key) => {
- const value = obj[key];
- const config = (propsConfig || {})[key] || {};
- /**
- * wrap functions in a function that ensures they are scope.$applied
- * ensures that when function is called from a React component
- * the Angular digest cycle is run
- */
- prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value;
- return prev;
- }, {});
- }
- /**
- *
- * @param watchDepth (value of HTML watch-depth attribute)
- * @param scope (angular scope)
- *
- * Uses the watchDepth attribute to determine how to watch props on scope.
- * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value
- */
- function watchProps(watchDepth, scope, watchExpressions, listener) {
- const supportsWatchCollection = angular.isFunction(scope.$watchCollection);
- const supportsWatchGroup = angular.isFunction(scope.$watchGroup);
- const watchGroupExpressions = [];
- watchExpressions.forEach(expr => {
- const actualExpr = getPropExpression(expr);
- const exprWatchDepth = getPropWatchDepth(watchDepth, expr);
- if (exprWatchDepth === 'collection' && supportsWatchCollection) {
- scope.$watchCollection(actualExpr, listener);
- } else if (exprWatchDepth === 'reference' && supportsWatchGroup) {
- watchGroupExpressions.push(actualExpr);
- } else if (exprWatchDepth === 'one-time') {
- //do nothing because we handle our one time bindings after this
- } else {
- scope.$watch(actualExpr, listener, exprWatchDepth !== 'reference');
- }
- });
- if (watchDepth === 'one-time') {
- listener();
- }
- if (watchGroupExpressions.length) {
- scope.$watchGroup(watchGroupExpressions, listener);
- }
- }
- // render React component, with scope[attrs.props] being passed in as the component props
- function renderComponent(component, props, scope, elem) {
- scope.$evalAsync(() => {
- ReactDOM.render(React.createElement(component, props), elem[0]);
- });
- }
- // get prop name from prop (string or array)
- function getPropName(prop) {
- return Array.isArray(prop) ? prop[0] : prop;
- }
- // get prop name from prop (string or array)
- function getPropConfig(prop) {
- return Array.isArray(prop) ? prop[1] : {};
- }
- // get prop expression from prop (string or array)
- function getPropExpression(prop) {
- return Array.isArray(prop) ? prop[0] : prop;
- }
- // find the normalized attribute knowing that React props accept any type of capitalization
- function findAttribute(attrs, propName) {
- const index = Object.keys(attrs).filter(attr => {
- return attr.toLowerCase() === propName.toLowerCase();
- })[0];
- return attrs[index];
- }
- // get watch depth of prop (string or array)
- function getPropWatchDepth(defaultWatch, prop) {
- const customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth;
- return customWatchDepth || defaultWatch;
- }
- // # reactComponent
- // Directive that allows React components to be used in Angular templates.
- //
- // Usage:
- // <react-component name="Hello" props="name"/>
- //
- // This requires that there exists an injectable or globally available 'Hello' React component.
- // The 'props' attribute is optional and is passed to the component.
- //
- // The following would would create and register the component:
- //
- // var module = angular.module('ace.react.components');
- // module.value('Hello', React.createClass({
- // render: function() {
- // return <div>Hello {this.props.name}</div>;
- // }
- // }));
- //
- const reactComponent = $injector => {
- return {
- restrict: 'E',
- replace: true,
- link: function(scope, elem, attrs) {
- const reactComponent = getReactComponent(attrs.name, $injector);
- const renderMyComponent = () => {
- const scopeProps = scope.$eval(attrs.props);
- const props = applyFunctions(scopeProps, scope);
- renderComponent(reactComponent, props, scope, elem);
- };
- // If there are props, re-render when they change
- attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent();
- // cleanup when scope is destroyed
- scope.$on('$destroy', () => {
- if (!attrs.onScopeDestroy) {
- ReactDOM.unmountComponentAtNode(elem[0]);
- } else {
- scope.$eval(attrs.onScopeDestroy, {
- unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
- });
- }
- });
- },
- };
- };
- // # reactDirective
- // Factory function to create directives for React components.
- //
- // With a component like this:
- //
- // var module = angular.module('ace.react.components');
- // module.value('Hello', React.createClass({
- // render: function() {
- // return <div>Hello {this.props.name}</div>;
- // }
- // }));
- //
- // A directive can be created and registered with:
- //
- // module.directive('hello', function(reactDirective) {
- // return reactDirective('Hello', ['name']);
- // });
- //
- // Where the first argument is the injectable or globally accessible name of the React component
- // and the second argument is an array of property names to be watched and passed to the React component
- // as props.
- //
- // This directive can then be used like this:
- //
- // <hello name="name"/>
- //
- const reactDirective = $injector => {
- return (reactComponentName, props, conf, injectableProps) => {
- const directive = {
- restrict: 'E',
- replace: true,
- link: function(scope, elem, attrs) {
- const reactComponent = getReactComponent(reactComponentName, $injector);
- // if props is not defined, fall back to use the React component's propTypes if present
- props = props || Object.keys(reactComponent.propTypes || {});
- // for each of the properties, get their scope value and set it to scope.props
- const renderMyComponent = () => {
- let scopeProps = {};
- const config = {};
- props.forEach(prop => {
- const propName = getPropName(prop);
- scopeProps[propName] = scope.$eval(findAttribute(attrs, propName));
- config[propName] = getPropConfig(prop);
- });
- scopeProps = applyFunctions(scopeProps, scope, config);
- scopeProps = angular.extend({}, scopeProps, injectableProps);
- renderComponent(reactComponent, scopeProps, scope, elem);
- };
- // watch each property name and trigger an update whenever something changes,
- // to update scope.props with new values
- const propExpressions = props.map(prop => {
- return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
- });
- // If we don't have any props, then our watch statement won't fire.
- props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent();
- // cleanup when scope is destroyed
- scope.$on('$destroy', () => {
- if (!attrs.onScopeDestroy) {
- ReactDOM.unmountComponentAtNode(elem[0]);
- } else {
- scope.$eval(attrs.onScopeDestroy, {
- unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]),
- });
- }
- });
- },
- };
- return angular.extend(directive, conf);
- };
- };
- const ngModule = angular.module('react', []);
- ngModule.directive('reactComponent', ['$injector', reactComponent]);
- ngModule.factory('reactDirective', ['$injector', reactDirective]);
|