From 3fa75d54e6f95ae2fea2503728cadd5d039d58fd Mon Sep 17 00:00:00 2001 From: juspears Date: Tue, 1 Dec 2015 17:09:16 -0800 Subject: [PATCH] Added expression support --- Readme.md | 70 +++++++++++++++++++++++++++++- public/samples/CustomType-setup.js | 7 +-- public/samples/Expression-setup.js | 18 ++++++++ public/samples/Expression.js | 21 +++++++++ src/PropTypes.js | 15 ++++++- src/components/Editor.jsx | 67 +++++++++++++++++++++++----- test/components/Editor-test.jsx | 39 ++++++++++++++++- 7 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 public/samples/Expression-setup.js create mode 100644 public/samples/Expression.js diff --git a/Readme.md b/Readme.md index 69aceb7..3e40875 100644 --- a/Readme.md +++ b/Readme.md @@ -494,7 +494,7 @@ Props: * dismiss (optional - string) - This will be set to false, given to make the current conditional false. Set this for dismissing modals. If not provided, the template will recieve a dismiss property with the current key substituting '.' with '_' and prepended with an @ -See the [example](http://subschema.github.io/subschema/#/Conditional) +See the [example](https://subschema.github.io/subschema/#/Conditional) @@ -622,6 +622,74 @@ Types get passed value along with any other properties descriped in the static p implement anything, other than React.Component. State is managed by the editor. +## Expression Properties +Occasionally it would be nice to bind the value of a property to the value manager. We got you covered. To make a +property of a custom component you can use the substitution language used in the Content object. As of now, none of the +default components take the expression syntax. This may change in the future. It would pretty easy to extend the +propTypes on existing components to make thier values dynamic. + +Example: +```jsx + + @provide.type + class Anchor extends React.Component { + static propTypes = { + //by making this propType an expression it will evaluate it dynamically. + href:PropTypes.expression, + label:PropTypes.string + }; + static defaultProps = { + href:'/somewhere/{..page}' + } + render(){ + return {this.props.label} + } + } +``` + +Now the {..page} will be substituted with the page value in the valueManager. You can of course override the +default prop in the schema. Note the .. makes it look up a level for the value. No dots means look in the +current path + name, a single dot, is the current path. This is follows the listener conventions elsewhere. + +Example Usage: +```jsx + + var schema = { + + schema:{ + selectPage:{ + type:'Select', + options:'Page1, Page2, Page3' + }, + link1:{ + type:'Anchor', + label:'Go To Page', + href:'/{..selectPage}/index.html' + } + }, + fields:"selectPage, link1" + } + +
+``` + +Now when a user changes the selectPage, then the Anchor (link1} will reflect said change. + +The default substitution engine can be changed by setting expressionEngine on Editor +``` + import {Editor} from "Subschema"; + + Editor.expressionEngine = function() { + return { + format(string){ + listen:[]//array of paths to listen to. + } + } + + +``` + +See the [example](https://subschema.github.io/subschema/#/Expression) diff --git a/public/samples/CustomType-setup.js b/public/samples/CustomType-setup.js index 4b0e8a1..6ad7388 100644 --- a/public/samples/CustomType-setup.js +++ b/public/samples/CustomType-setup.js @@ -90,14 +90,15 @@ var styles = { width: '16px', boxSizing: 'border-box', borderRadius: '8px', - border: '1px outset #fff', + border: '5px outset rgba(204, 204, 204, .4)', position: 'absolute', - backgroundColor: '#ccc', transition: 'all .2s', }, buttonOn: { - left: 1 + left: 1, + border:'5px outset rgba(255,255,255,.8)' + }, buttonOff: { left: '100%', diff --git a/public/samples/Expression-setup.js b/public/samples/Expression-setup.js new file mode 100644 index 0000000..f6d3e33 --- /dev/null +++ b/public/samples/Expression-setup.js @@ -0,0 +1,18 @@ +var {PropTypes, decorators} = Subschema; +var {provide} = decorators; + +@provide.type +class Anchor extends React.Component { + static propTypes = { + //by making this propType an expression it will evaluate it dynamically. + href:PropTypes.expression, + label:PropTypes.expression + }; + static defaultProps = { + href:'/somewhere/{..page}' + } + render(){ + return {this.props.label} + } +} + diff --git a/public/samples/Expression.js b/public/samples/Expression.js new file mode 100644 index 0000000..31225ae --- /dev/null +++ b/public/samples/Expression.js @@ -0,0 +1,21 @@ +module.exports = { + description: 'Shows how you can use Expressions on Custom Types', + schema: { + schema:{ + selectPage:{ + type:'Select', + options:'Content, Conditional, Basic, Autocomplete' + }, + link1:{ + type:'Anchor', + label:'Go To {..selectPage}', + href:'/#/{..selectPage}' + } + }, + fields:"selectPage, link1" + }, + data: { + selectPage: 'Content' + }, + setupTxt: require('!!raw!./Expression-setup.js') +} diff --git a/src/PropTypes.js b/src/PropTypes.js index abefd2e..fd9b931 100644 --- a/src/PropTypes.js +++ b/src/PropTypes.js @@ -20,6 +20,7 @@ function customPropType(type, name) { } var api = extend({}, PropTypes); +api.promise = api.shape({then: api.func}); api.id = customPropType(api.string, 'id'); @@ -59,6 +60,17 @@ api.dataType = customPropType(api.string, 'dataType'); api.type = api.oneOfType([api.string, api.func]); +/** + * Signify this property can take an expression. This + * allows properties to be tied to the valueManager. So + * it will evaluate the property against the valueManager. + * + * It will add a listener for each of the corresponding + * matching strings. + * + */ +api.expression = customPropType(api.string, 'expression'); + api.loader = api.shape({ loadTemplate: api.func, loadType: api.func, @@ -156,7 +168,6 @@ api.schema = api.oneOfType([api.string, api.shape({ })]); - api.validators = api.oneOfType([api.arrayString, api.arrayOf(api.validators)]); api.operator = api.oneOfType([api.string, api.func, api.instanceOf(RegExp)]); @@ -214,7 +225,7 @@ api.propTypeToName = function propTypeToName(propType) { } } }; -api.promise = api.shape({then: api.func}); + api.propTypesToNames = function (props) { var ret = {}; map(props, function (v, k) { diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index ef0c62e..55fb494 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -5,7 +5,9 @@ import Template from './Template.jsx'; import {listeners} from './../decorators'; import defaults from 'lodash/object/defaults'; import {forField} from '../css'; -import { FREEZE_OBJ, nextFunc, FREEZE_ARR, noop, titlelize, isString,toArray,nullCheck} from '../tutils'; +import { FREEZE_OBJ, applyFuncs, each, nextFunc, FREEZE_ARR, noop, titlelize, isString,toArray,nullCheck} from '../tutils'; +import substitute from '../types/SubstituteMixin'; +import warning from '../warning'; const ERRORS = { '.': 'setErrors' @@ -50,6 +52,8 @@ export default class Editor extends Component { static contextTypes = PropTypes.contextTypes; + static expressionEngine = substitute; + static defaultProps = { field: { type: 'Text' @@ -81,7 +85,7 @@ export default class Editor extends Component { hasChanged: false, isValid: false }; - + this._substitute = []; this.initValidators(props, context); this.initPropTypes(props, context); } @@ -98,6 +102,15 @@ export default class Editor extends Component { this.validators = context.loader.loadByPropType(PropTypes.validators, props.field.validators); } + addExpression(property, expression) { + expression = Editor.expressionEngine(expression); + if (!this._expressions) { + this._expressions = {[property]: expression}; + } else { + warning(this._expressions[property] == null, 'Multiple expressions for the same property %s?', property); + this._expressions[property] = expression; + } + } initPropTypes(props, context) { var Component = this._Component = this.createPropForType(props, context); @@ -142,7 +155,7 @@ export default class Editor extends Component { } else if (generatedDefs.hasOwnProperty(key)) { val = generatedDefs[key]; } - return this.normalizePropType(propType, val); + return this.normalizePropType(propType, val, key); }, this, propTypes, Editor.fieldPropTypes); //change in behaviour fieldAttrs come afterward @@ -154,15 +167,17 @@ export default class Editor extends Component { return Node; } - normalizePropType(propType, value) { + normalizePropType(propType, value, property) { if (propType === PropTypes.valueEvent || propType === PropTypes.valueEvent.isRequired) { return nextFunc(value, this.handleUpdateValue); } else if (propType === PropTypes.targetEvent || propType === PropTypes.targetEvent.isRequired) { return nextFunc(value, this.handleTargetValue); } else if (propType === PropTypes.blurEvent || propType === PropTypes.blurEvent.isRequired) { return nextFunc(value, this.handleValidateListener); - } else if (propType === PropTypes.validEvent || propType === PropTypes.validEvent.isRequired){ + } else if (propType === PropTypes.validEvent || propType === PropTypes.validEvent.isRequired) { return nextFunc(value, this.handleValid); + } else if (propType === PropTypes.expression || propType === PropTypes.expression.isRequired) { + this.addExpression(property, value); } return this.context.loader.loadByPropType(propType, value); } @@ -216,14 +231,37 @@ export default class Editor extends Component { if (this._Component.isContainer) { return EMPTY; } + var listeners; + if (this.props.field.conditional && this.props.field.conditional.path) { - return { - [this.props.field.conditional.path]: 'handleValueChange' + if (!listeners) listeners = {}; + listeners[this.props.field.conditional.path] = 'handleValueChange'; + } + + if (this._expressions) { + if (!listeners) { + var {...listeners} = VALUES; } + //Go through each property expression pair and store the state. + each(this._expressions, (expression, property)=> { + expression.listen.reduce((obj, key)=> { + obj[key] = applyFuncs((v)=> { + if (!expression.state) { + expression.state = {[key]: v}; + } else { + expression.state[key] = v; + } + this.forceUpdate(); + }, obj[key]); + + return obj; + }, listeners); + }); } - return VALUES; + return listeners || VALUES; } + @listeners("error") listenToError() { if (this._Component.isContainer) { @@ -295,6 +333,9 @@ export default class Editor extends Component { this.setState({valid}) } + _invokeExpression(expression, property) { + this[property] = expression.format(expression.state); + } render() { var {field} = this.props, props = this.props; @@ -307,8 +348,15 @@ export default class Editor extends Component { errorClassName = errorClassName == null ? 'has-error' : errorClassName, Component = this._Component; + var expressions; + if (this._expressions) { + expressions = {}; + each(this._expressions, this._invokeExpression, expressions); + } else { + expressions = FREEZE_OBJ; + } - var child = ; if (title === false) { title = ''; @@ -321,7 +369,6 @@ export default class Editor extends Component { } var errors = this.state.errors, error; if (errors) error = errors[0] && errors[0].message || errors[0]; - return