Home Manual Reference Source Test

src/props.js

import React from 'react';
import { getRenderedJSON, } from './main';
import * as utilities from './utils';
import { getComponentFromMap, getReactFunctionComponent, getReactContext, } from './components';
// if (typeof window === 'undefined') {
//   var window = window || {};
// }

//https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically
export const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
export const ARGUMENT_NAMES = /([^\s,]+)/g;
/**
 * returns the names of parameters from a function declaration
 * @example
 * const arrowFunctionAdd = (a,b)=>a+b;
 * function regularFunctionAdd(c,d){return c+d;}
 * getParamNames(arrowFunctionAdd) // => ['a','b']
 * getParamNames(regularFunctionAdd) // => ['c','d']
 * @param {Function} func 
 * @todo write tests
 */
export function getParamNames(func) {
  var fnStr = func.toString().replace(STRIP_COMMENTS, '');
  var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
  if(result === null){
    result = [];
  }
  return result;
}

/**
 * It uses traverse on a traverseObject to returns a resolved object on propName. So if you're making an ajax call and want to pass properties into a component, you can assign them using asyncprops and reference object properties by an array of property paths
 * @param {Object} [traverseObject={}] - the object that contains values of propName
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @param {Object} [options.propName='asyncprops'] - Property on JSONX to resolve values onto, i.e (asyncprops,thisprops,windowprops) 
 * @returns {Object} resolved object
 * @example
 const traverseObject = {
  user: {
    name: 'jsonx',
    description: 'react withouth javascript',
  },
  stats: {
    logins: 102,
    comments: 3,
  },
  authentication: 'OAuth2',
};
const testJSONX = {
  component: 'div',
  props: {
    id: 'generatedJSONX',
    className:'jsonx',
  },
  asyncprops:{
    auth: [ 'authentication', ],
    username: [ 'user', 'name', ],
  },
  children: [
    {
      component: 'p',
      props: {
        style: {
          color: 'red',
          fontWeight:'bold',
        },
      },
      children:'hello world',
    },
  ],
};
const JSONXP = getJSONXProps({ jsonx: testJSONX, traverseObject, });
// => {
//   auth: 'OAuth2',
//   username: 'jsonx'
// }

//finally resolves:
const testJSONX = {
  component: 'div',
  props: {
    id: 'generatedJSONX',
    className:'jsonx',
    auth: 'OAuth2',
    username: 'jsonx',
  },
  children: [
    {
      component: 'p',
      props: {
        style: {
          color: 'red',
          fontWeight:'bold',
        },
      },
      children:'hello world',
    },
  ],
};
 */
export function getJSONXProps(options = {}) {
  // eslint-disable-next-line
  let { jsonx = {}, propName = 'asyncprops', traverseObject = {}, } = options;
  // return (jsonx.asyncprops && typeof jsonx.asyncprops === 'object')
  // ? utilities.traverse(jsonx.asyncprops, resources)
  // : {};
  return (jsonx[ propName ] && typeof jsonx[ propName ] === 'object')
    ? utilities.traverse(jsonx[ propName ], traverseObject)
    : {};
}

/**
 * returns children jsonx components defined on __spreadComponent spread over an array on props.__spread
 * @param {*} options 
 */
export function getChildrenComponents(options = {}) {
  const { allProps = {}, jsonx = {}, } = options;
  // const asyncprops = getJSONXProps({ jsonx, propName: 'spreadprops', traverseObject: allProps, });
  if (Array.isArray(allProps.__spread) === false) {
    if ((this && this.debug) || jsonx.debug) {
      return {
        children: new Error('Using __spreadComponent requires an array prop \'__spread\'').toString(),
      };
    } else {
      return { children:undefined, };
    }
  } else {
    return {
      _children: allProps.__spread.map(__item => {
        const clonedChild = Object.assign({}, jsonx.__spreadComponent);
        const clonedChildProps = Object.assign({}, clonedChild.props);
        clonedChildProps.__item = __item;
        clonedChild.props = clonedChildProps;
        return clonedChild;
      }),
    };
  }
}

export function boundArgsReducer(jsonx = {}) {
  return (args, arg) => {
    let val;
    if (this && this.state && typeof this.state[ arg ] !== 'undefined') val = (this.state[ arg ]);
    else if (this && this.props && typeof this.props[ arg ] !== 'undefined') val = (this.props[ arg ]);
    else if (jsonx.props && typeof jsonx.props[ arg ] !== 'undefined') val = (jsonx.props[ arg ]);
    if (typeof val !== 'undefined') args.push(val);
    return args.filter(a=>typeof a!=='undefined');
  };
}

/**
 * Used to evalute javascript and set those variables as props. getEvalProps evaluates __dangerouslyEvalProps and __dangerouslyBindEvalProps properties with eval, this is used when component properties are functions, __dangerouslyBindEvalProps is used when those functions require that this is bound to the function. For __dangerouslyBindEvalProps it must resolve an expression, so functions should be wrapped in (). I.e. (function f(x){ return this.minimum+x;})
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @returns {Object} returns resolved object with evaluated javascript
 * @example
 const testVals = {
    auth: 'true',
    username: '(user={})=>user.name',
  };
  const testJSONX = Object.assign({}, sampleJSONX, {
    __dangerouslyEvalProps: testVals, __dangerouslyBindEvalProps: {
      email: '(function getUser(user={}){ return this.testBound(); })',
    },
  });
  const JSONXP = getEvalProps.call({ testBound: () => 'bounded', }, { jsonx: testJSONX, });
  const evalutedComputedFunc = JSONXP.username({ name: 'bob', });
  const evalutedComputedBoundFunc = JSONXP.email({ email:'test@email.domain', });
  // expect(JSONXP.auth).to.be.true;
  // expect(evalutedComputedFunc).to.eql('bob');
  // expect(evalutedComputedBoundFunc).to.eql('bounded');
 */
export function getEvalProps(options = {}) {
  const { jsonx, } = options;
  const scopedEval = eval; //https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval
  let evAllProps = {};
  if (jsonx.__dangerouslyEvalAllProps) {
    let evVal;
    try {
      // eslint-disable-next-line
      evVal = (typeof evVal === 'function')
        ? jsonx.__dangerouslyEvalAllProps
        : scopedEval(jsonx.__dangerouslyEvalAllProps);
    } catch (e) { 
      if (this.debug || jsonx.debug) evVal = e;
    }
    evAllProps = evVal.call(this, { jsonx, });
  }
  const evProps = Object.keys(jsonx.__dangerouslyEvalProps || {}).reduce((eprops, epropName) => {
    let evVal;
    let evValString;
    try {
      // eslint-disable-next-line
      evVal = scopedEval(jsonx.__dangerouslyEvalProps[ epropName ]);
      evValString = evVal.toString();
    } catch (e) { 
      if (this.debug || jsonx.debug) evVal = e;
    }
    eprops[ epropName ] = (typeof evVal === 'function')
      ? evVal.call(this, { jsonx, })
      : evVal;
    if (this.exposeEval) eprops[ `__eval_${epropName}` ] = evValString;
    return eprops;
  }, {});
  const evBindProps = Object.keys(jsonx.__dangerouslyBindEvalProps || {}).reduce((eprops, epropName) => {
    let evVal;
    let evValString;

    try {
      let args;
      const functionBody = jsonx.__dangerouslyBindEvalProps[ epropName ];
      // InlineFunction = Function.prototype.constructor.apply({}, args);
      let functionDefinition;
      if (typeof functionBody === 'function') {
        functionDefinition = functionBody;
      } else {
        functionDefinition = scopedEval(jsonx.__dangerouslyBindEvalProps[ epropName ]);
        evValString = functionDefinition.toString();

      } // eslint-disable-next-line
      if (jsonx.__functionargs && jsonx.__functionargs[epropName]) {
        args = [this, ].concat(jsonx.__functionargs[epropName].reduce(boundArgsReducer.call(this, jsonx), []));
      } else if (jsonx.__functionparams===false) {
        args = [this, ];
      } else {
        const functionDefArgs = getParamNames(functionDefinition);
        args = [this, ].concat(functionDefArgs.reduce(boundArgsReducer.call(this, jsonx), []));
      }
      // eslint-disable-next-line
      evVal = functionDefinition.bind(...args);
    } catch (e) { 
      if (this.debug || jsonx.debug) evVal = e;
    }
    // eslint-disable-next-line
    eprops[ epropName ] = evVal;
    if (this.exposeEval) eprops[ `__eval_${epropName}` ] = evValString;
    return eprops;
  }, {});

  return Object.assign({}, evProps, evBindProps, evAllProps);
}

/**
 * Resolves jsonx.__dangerouslyInsertComponents into an object that turns each value into a React components. This is typically used in a library like Recharts where you pass custom components for chart ticks or plot points. 
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @param {Object} [options.resources={}] - object to use for resourceprops(asyncprops), usually a result of an asynchronous call
 * @returns {Object} resolved object of React Components
 */
export function getComponentProps(options = {}) {
  const { jsonx, resources, } = options;
  return Object.keys(jsonx.__dangerouslyInsertComponents).reduce((cprops, cpropName) => {
    let componentVal;
    try {
      // eslint-disable-next-line
      componentVal = getRenderedJSON.call(this, jsonx.__dangerouslyInsertComponents[ cpropName ], resources);
    } catch (e) {
      if (this.debug || jsonx.debug) componentVal = e;
    }
    cprops[ cpropName ] = componentVal;
    return cprops;
  }, {});
}

export function getReactComponents(options) {
  const { jsonx, resources, } = options;
  const functionComponents = (!jsonx.__dangerouslyInsertFunctionComponents)
    ? {}
    : Object.keys(jsonx.__dangerouslyInsertFunctionComponents).reduce((cprops, cpropName) => {
      let componentVal;
      try {
        const args = jsonx.__dangerouslyInsertFunctionComponents[ cpropName ];
        args.options = Object.assign({}, args.options, { resources });
        // eslint-disable-next-line
        componentVal = getReactFunctionComponent.call(this, args.reactComponent, args.functionBody, args.options);
      } catch (e) {
        if (this.debug || jsonx.debug) componentVal = e;
      }
      cprops[ cpropName ] = cpropName === '_children' ? [ componentVal ] : componentVal;
      return cprops;
    }, {});
  const classComponents = (!jsonx.__dangerouslyInsertClassComponents)
    ? {}
    : Object.keys(jsonx.__dangerouslyInsertClassComponents).reduce((cprops, cpropName) => {
      let componentVal;
      try {
        const args = jsonx.__dangerouslyInsertClassComponents[ cpropName ];
        args.options = Object.assign({}, args.options, { resources });
        // eslint-disable-next-line
        componentVal = getReactFunctionComponent.call(this, args.reactComponent, args.options);
      } catch (e) {
        if (this.debug || jsonx.debug) componentVal = e;
      }
      cprops[ cpropName ] = cpropName === '_children' ? [ componentVal ] : componentVal;
      return cprops;
    }, {});
  return Object.assign({}, functionComponents, classComponents);
}

/**
 * Resolves jsonx.__dangerouslyInsertReactComponents into an object that turns each value into a React components. This is typically used in a library like Recharts where you pass custom components for chart ticks or plot points. 
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
//  * @param {Object} [options.resources={}] - object to use for asyncprops, usually a result of an asynchronous call
 * @returns {Object} resolved object of React Components
 */
export function getReactComponentProps(options = {}) {
  const { jsonx, } = options;
  if (jsonx.__dangerouslyInsertJSONXComponents && Object.keys(jsonx.__dangerouslyInsertJSONXComponents).length) { 
    return Object.keys(jsonx.__dangerouslyInsertJSONXComponents).reduce((cprops, cpropName) => {
      let componentVal;
      try {
        componentVal = getComponentFromMap({
          jsonx: jsonx.__dangerouslyInsertJSONXComponents[ cpropName ],
          reactComponents: this.reactComponents,
          componentLibraries: this.componentLibraries,
        });
      } catch (e) {
        if (this.debug || jsonx.debug) componentVal = e;
      }
      // eslint-disable-next-line
      cprops[ cpropName ] = componentVal;
      return cprops;
    }, {});
  } else {
    return Object.keys(jsonx.__dangerouslyInsertReactComponents).reduce((cprops, cpropName) => {
      let componentVal;
      try {
        componentVal = getComponentFromMap({
          jsonx: {
            component: jsonx.__dangerouslyInsertReactComponents[ cpropName ],
            props: jsonx.__dangerouslyInsertComponentProps
              ? jsonx.__dangerouslyInsertComponentProps[ cpropName ]
              : {},
          },
          reactComponents: this.reactComponents,
          componentLibraries: this.componentLibraries,
        });
      } catch (e) {
        if (this.debug || jsonx.debug) componentVal = e;
      }
      // eslint-disable-next-line
      cprops[ cpropName ] = componentVal;
      return cprops;
    }, {});
  }
}

/**
 * Takes a function string and returns a function on either this.props or window. The function can only be 2 levels deep
 * @param {Object} options 
 * @param {String} [options.propFunc='func:'] - function string, like func:window.LocalStorage.getItem or func:this.props.onClick  or func:inline.myInlineFunction
 * @param {Object} [options.allProps={}] - merged computed props, Object.assign({ key: renderIndex, }, thisprops, jsonx.props, resourceprops, asyncprops, windowprops, evalProps, insertedComponents);
 * @returns {Function} returns a function from this.props or window functions
 * @example
 * getFunctionFromProps({ propFunc='func:this.props.onClick', }) // => this.props.onClick
 */
export function getFunctionFromProps(options) {
  const { propFunc='func:', propBody, jsonx, functionProperty='', } = options;
  // eslint-disable-next-line
  const { logError = console.error,  debug, } = this;
  const windowObject = this.window || global.window || {};
  try {
    const functionNameString = propFunc.split(':')[ 1 ] || '';
    const functionNameArray = functionNameString.split('.');
    const functionName = (functionNameArray.length) ? functionNameArray[ functionNameArray.length - 1 ] : '';

    if (propFunc.includes('func:inline')) {
      // eslint-disable-next-line
      let InlineFunction;
      if (jsonx.__functionargs) {
        const args = [].concat(jsonx.__functionargs[functionProperty]);
        args.push(propBody);
        InlineFunction = Function.prototype.constructor.apply({}, args);
      } else {
        InlineFunction = Function('param1', 'param2', '"use strict";' + propBody);
      }
      const [propFuncName, funcName, ] = propFunc.split('.');
      
      Object.defineProperty(
        InlineFunction,
        'name',
        {
          value: funcName,
        }
      );
      if (jsonx.__functionargs) {
        const boundArgs = [this,].concat(jsonx.__functionargs[functionProperty].map(arg => jsonx.props[ arg ]));
        return InlineFunction.bind(...boundArgs);
      } else {
        return InlineFunction.bind(this);
      }
    } else if (propFunc.indexOf('func:window') !== -1) {
      if (functionNameArray.length === 3) {
        try {
          return windowObject[ functionNameArray[ 1 ] ][ functionName ].bind(this);
        } catch (e) {
          if (debug) {
            logError(e);
          }
          return windowObject[ functionNameArray[ 1 ] ][ functionName ];
        }
      } else {
        try {
          return windowObject[ functionName ].bind(this);
        } catch (e) {
          if (debug) {
            logError(e);
          }
          return windowObject[ functionName ];
        }
      }
    } else if (functionNameArray.length === 4) {
      return (this.props)
        ? this.props[ functionNameArray[ 2 ] ][ functionName ]
        : jsonx.props[ functionNameArray[ 2 ] ][ functionName ];
    } else if (functionNameArray.length === 3) {
      return (this.props)
        ? this.props[ functionName ].bind(this)
        : jsonx.props[ functionName ].bind(this);
    } else {
      return function () {};
    }
  } catch (e) {
    if (this.debug){
      logError(e);
      if (jsonx && jsonx.debug) return e;
    }
    return function () {};
  }
}

/**
 * Returns a resolved object from function strings that has functions pulled from jsonx.__functionProps
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @param {Object} [options.allProps={}] - merged computed props, Object.assign({ key: renderIndex, }, thisprops, jsonx.props, asyncprops, windowprops, evalProps, insertedComponents);
 * @returns {Object} resolved object of functions from function strings
 */
export function getFunctionProps(options = {}) {
  const { allProps = {}, jsonx = {}, } = options;
  const getFunction = getFunctionFromProps.bind(this);
  const funcProps = jsonx.__functionProps;
  //Allowing for window functions
  Object.keys(funcProps).forEach(key => {
    if (typeof funcProps[ key ] === 'string' && funcProps[ key ].indexOf('func:') !== -1) {
      allProps[ key ] = getFunction({
        propFunc: funcProps[ key ],
        propBody: (jsonx.__inline)?jsonx.__inline[ key ]:'',
        jsonx,
        functionProperty:key,
      });
    } 
  });
  return allProps;
}

/**
 * Returns a resolved object that has React Components pulled from window.__jsonx_custom_elements
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @param {Object} [options.allProps={}] - merged computed props, Object.assign({ key: renderIndex, }, thisprops, jsonx.props, asyncprops, windowprops, evalProps, insertedComponents);
 * @returns {Object} resolved object of with React Components from a window property window.__jsonx_custom_elements
 */
export function getWindowComponents(options = {}) {
  const { allProps, jsonx, } = options;
  const windowComponents = jsonx.__windowComponents;
  const window = this.window || global.window || {};
  const windowFuncPrefix = 'func:window.__jsonx_custom_elements';
  // if (jsonx.hasWindowComponent && window.__jsonx_custom_elements) {
  Object.keys(windowComponents).forEach(key => {
    const windowKEY = (typeof windowComponents[ key ] === 'string')
      ? windowComponents[ key ].replace(`${windowFuncPrefix}.`, '')
      : '';
    if (typeof windowComponents[ key ] === 'string' && windowComponents[ key ].indexOf(windowFuncPrefix) !== -1 && typeof window.__jsonx_custom_elements[ windowKEY ] === 'function') {
      const windowComponentElement = window.__jsonx_custom_elements[ windowKEY ];
      const windowComponentProps = (allProps[ '__windowComponentProps' ]) ? allProps[ '__windowComponentProps' ]
        : this.props;
      allProps[ key ] = React.createElement(
        windowComponentElement,
        windowComponentProps,
        null);
    }
  });
  return allProps;
}

/**
 * Returns computed properties for React Components and any property that's prefixed with __ is a computedProperty
 * @param {Object} options 
 * @param {Object} options.jsonx - Valid JSONX JSON 
 * @param {Object} [options.resources={}] - object to use for asyncprops, usually a result of an asynchronous call
 * @param {Number} options.renderIndex - number used for React key prop
 * @param {function} [options.logError=console.error] - error logging function
 * @param {Object} [options.componentLibraries] - react components to render with JSONX
 * @param {Boolean} [options.useReduxState=true] - use redux props in this.props
 * @param {Boolean} [options.ignoreReduxPropsInComponentLibraries=true] - ignore redux props in this.props for component libraries, this is helpful incase these properties collide with component library element properties
 * @param {boolean} [options.debug=false] - use debug messages
 * @example
const testJSONX = { component: 'div',
  props: { id: 'generatedJSONX', className: 'jsonx' },
  children: [ [Object] ],
  asyncprops: { auth: [Array], username: [Array] },
  __dangerouslyEvalProps: { getUsername: '(user={})=>user.name' },
  __dangerouslyInsertComponents: { myComponent: [Object] } 
const resources = {
  user: {
    name: 'jsonx',
    description: 'react withouth javascript',
  },
  stats: {
    logins: 102,
    comments: 3,
  },
  authentication: 'OAuth2',
};
const renderIndex = 1;
getComputedProps.call({}, {
        jsonx: testJSONX,
        resources,
        renderIndex,
      });
computedProps = { key: 1,
     id: 'generatedJSONX',
     className: 'jsonx',
     auth: 'OAuth2',
     username: 'jsonx',
     getUsername: [Function],
     myComponent:
      { '$$typeof': Symbol(react.element),
        type: 'p',
        key: '8',
        ref: null,
        props: [Object],
        _owner: null,
        _store: {} } } }
 *
 */
export function getComputedProps(options = {}) {
  // eslint-disable-next-line
  const { jsonx = {}, resources = {}, renderIndex, logError = console.error, useReduxState=true, ignoreReduxPropsInComponentLibraries=true, disableRenderIndexKey=true, componentLibraries, debug, } = options;
  try {
    const componentThisProp = (jsonx.thisprops)
      ? Object.assign({
        __jsonx: {
          _component: jsonx,
          _resources: resources,
        },
      }, this.props,
      jsonx.props,
      (useReduxState && !jsonx.ignoreReduxProps && (ignoreReduxPropsInComponentLibraries && !componentLibraries[ jsonx.component ]))
        ? (this.props && this.props.getState) ? this.props.getState() : {}
        : {}
      )
      : undefined;
    const windowTraverse = typeof window !== 'undefined' ? window : {};
    const asyncprops = jsonx.asyncprops ? getJSONXProps({ jsonx, propName: 'asyncprops', traverseObject: resources, }) : {};
    const resourceprops = jsonx.resourceprops ? getJSONXProps({ jsonx, propName: 'resourceprops', traverseObject: resources, }) : {};
    const windowprops = jsonx.windowprops ? getJSONXProps({ jsonx, propName: 'windowprops', traverseObject: windowTraverse, }) : {};
    const thisprops = jsonx.thisprops ? getJSONXProps({ jsonx, propName: 'thisprops', traverseObject: componentThisProp, }) : {};
    const thisstate = jsonx.thisstate ? getJSONXProps({ jsonx, propName: 'thisstate', traverseObject: this.state, }) : {};

    //allowing javascript injections
    const evalProps = (jsonx.__dangerouslyEvalProps || jsonx.__dangerouslyBindEvalProps)
      ? getEvalProps.call(this, { jsonx, })
      : {};
    const insertedComponents = (jsonx.__dangerouslyInsertComponents)
      ? getComponentProps.call(this, { jsonx, resources, debug, })
      : {};
    const insertedReactComponents = (jsonx.__dangerouslyInsertReactComponents || jsonx.__dangerouslyInsertJSONXComponents)
      ? getReactComponentProps.call(this, { jsonx, debug, })
      : {};
    const insertedComputedComponents = (jsonx.__dangerouslyInsertFunctionComponents  || jsonx.__dangerouslyInsertClassComponents)
      ? getReactComponents.call(this, { jsonx, debug, })
      : {};
    
    const evalAllProps = (jsonx.__dangerouslyEvalAllProps)
      ? getEvalProps.call(this, { jsonx, })
      : {};
    const allProps = Object.assign({}, this.disableRenderIndexKey || disableRenderIndexKey ? {}: { key: renderIndex, }, jsonx.props, thisprops, thisstate, resourceprops, asyncprops, windowprops, evalProps, insertedComponents, insertedReactComponents, insertedComputedComponents);
    const computedProps = Object.assign({}, allProps,
      jsonx.__functionProps ? getFunctionProps.call(this, { allProps, jsonx, }) : {},
      jsonx.__windowComponents ? getWindowComponents.call(this, { allProps, jsonx, }) : {},
      jsonx.__spreadComponent ? getChildrenComponents.call(this, { allProps, jsonx, }) : {},
      evalAllProps);
    if (jsonx.debug) console.debug({ jsonx, computedProps, });
    return computedProps;
  } catch (e) {
    debug && logError(e, (e.stack) ? e.stack : 'no stack');
    return null;
  }
}