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;
}
}