123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- //
- // 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 angular, { auto } from 'angular';
- import { kebabCase } from 'lodash';
- import React, { ComponentType } from 'react';
- import ReactDOM from 'react-dom';
- // get a react component from name (components can be an angular injectable e.g. value, factory or
- // available on window
- function getReactComponent(name: string | Function, $injector: auto.IInjectorService): ComponentType {
- // if name is a function assume it is component and return it
- if (angular.isFunction(name)) {
- return name as unknown as ComponentType;
- }
- // 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) => {
- // @ts-ignore
- return current[namePart];
- }, window);
- } catch (e) {}
- }
- if (!reactComponent) {
- throw Error('Cannot find react component ' + name);
- }
- return reactComponent as unknown as ComponentType;
- }
- // wraps a function with scope.$apply, if already applied just return
- function applied(fn: any, scope: any) {
- if (fn.wrappedInApply) {
- return fn;
- }
- // this had the equivalent of `eslint-disable-next-line prefer-arrow/prefer-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: any, scope: any, propsConfig?: any): object {
- 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
- */
- // @ts-ignore
- 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: string, scope: any, watchExpressions: any[], listener: any) {
- const supportsWatchCollection = angular.isFunction(scope.$watchCollection);
- const supportsWatchGroup = angular.isFunction(scope.$watchGroup);
- const watchGroupExpressions = [];
- for (const expr of watchExpressions) {
- const actualExpr = getPropExpression(expr);
- const exprWatchDepth = getPropWatchDepth(watchDepth, expr);
- // ignore empty expressions & expressions with functions
- if (!actualExpr || actualExpr.match(/\(.*\)/) || exprWatchDepth === 'one-time') {
- continue;
- }
- if (exprWatchDepth === 'collection' && supportsWatchCollection) {
- scope.$watchCollection(actualExpr, listener);
- } else if (exprWatchDepth === 'reference' && supportsWatchGroup) {
- watchGroupExpressions.push(actualExpr);
- } 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: any, props: object, scope: any, elem: Element[]) {
- scope.$evalAsync(() => {
- ReactDOM.render(React.createElement(component, props), elem[0]);
- });
- }
- // get prop name from prop (string or array)
- function getPropName(prop: any) {
- return Array.isArray(prop) ? prop[0] : prop;
- }
- // get prop name from prop (string or array)
- function getPropConfig(prop: any) {
- return Array.isArray(prop) ? prop[1] : {};
- }
- // get prop expression from prop (string or array)
- function getPropExpression(prop: any) {
- return Array.isArray(prop) ? prop[0] : prop;
- }
- /**
- * Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
- * kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
- * evaluated by the browser as such.
- * @param attrs All attributes of the component.
- * @param propName Name of the prop that react component expects.
- */
- function findAttribute(attrs: object, propName: string): string {
- const index = Object.keys(attrs).find((attr: any) => {
- return attr.toLowerCase() === propName.toLowerCase() || kebabCase(attr) === kebabCase(propName);
- });
- // @ts-ignore
- return attrs[index];
- }
- // get watch depth of prop (string or array)
- function getPropWatchDepth(defaultWatch: string, prop: string | any[]) {
- 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: any): any => {
- return {
- restrict: 'E',
- replace: true,
- link: function (scope: any, elem: Element[], attrs: any) {
- 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: auto.IInjectorService) => {
- return (reactComponentName: string, props: string[], conf: any, injectableProps: any) => {
- const directive = {
- restrict: 'E',
- replace: true,
- link: function (scope: any, elem: Element[], attrs: any) {
- 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: any = {};
- const config: any = {};
- 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)
- ? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
- : findAttribute(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]);
|