//
// 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:
//