Skip to content

Commit

Permalink
Added expression support
Browse files Browse the repository at this point in the history
  • Loading branch information
juspears committed Dec 2, 2015
1 parent d774273 commit 3fa75d5
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 17 deletions.
70 changes: 69 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)



Expand Down Expand Up @@ -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 <a href={this.props.href}>{this.props.label}</a>
}
}
```
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"
}

<Form schema={schema}/>
```
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)
Expand Down
7 changes: 4 additions & 3 deletions public/samples/CustomType-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
Expand Down
18 changes: 18 additions & 0 deletions public/samples/Expression-setup.js
Original file line number Diff line number Diff line change
@@ -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 <a href={this.props.href}>{this.props.label}</a>
}
}

21 changes: 21 additions & 0 deletions public/samples/Expression.js
Original file line number Diff line number Diff line change
@@ -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')
}
15 changes: 13 additions & 2 deletions src/PropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)]);
Expand Down Expand Up @@ -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) {
Expand Down
67 changes: 57 additions & 10 deletions src/components/Editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -50,6 +52,8 @@ export default class Editor extends Component {

static contextTypes = PropTypes.contextTypes;

static expressionEngine = substitute;

static defaultProps = {
field: {
type: 'Text'
Expand Down Expand Up @@ -81,7 +85,7 @@ export default class Editor extends Component {
hasChanged: false,
isValid: false
};

this._substitute = [];
this.initValidators(props, context);
this.initPropTypes(props, context);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 = <Component ref="field" {...this._componentProps}
var child = <Component ref="field" {...this._componentProps} {...expressions}
value={this.state.value}/>;
if (title === false) {
title = '';
Expand All @@ -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 <Template field={rfield} {...props} template={template} conditional={conditional} fieldClass={fieldClass}
title={title}
errorClassName={errorClassName}
Expand Down
39 changes: 38 additions & 1 deletion test/components/Editor-test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";
import {React, intoWithContext, findNode, TestUtils,expect, change, focus,blur, Simulate,byTag, byType, notByType} from '../support';
import {loader, loaderFactory, Editor, ValueManager, types, templates} from 'Subschema';
import {loader, loaderFactory, PropTypes, Editor, ValueManager, types, templates} from 'Subschema';
var {Text} = types;

describe('Editor', function () {
Expand Down Expand Up @@ -97,5 +97,42 @@ describe('Editor', function () {

});

it('should update expressions', function () {
class ExpressionTest extends React.Component {
static propTypes = {
stuff: PropTypes.expression,
other: PropTypes.expression,
when: PropTypes.expression
}
static defaultProps = {
other: '{..test}-abc',
when: '{..cando}-do'
}

render() {
return <div>{this.props.stuff}{this.props.other}</div>
}
}
var valueManager = ValueManager({cando:'do'});
var customLoader = loaderFactory([loader]);
customLoader.addType('ExpressionTest', ExpressionTest);
var editor = intoWithContext(<Editor
field={{type:'ExpressionTest', validators:['required'], stuff:'{..test}-dba'}}
path="test"/>, {
valueManager,
loader: customLoader
});

var et = byType(editor, ExpressionTest);

expect(et.props.other).toEqual('-abc');
expect(et.props.stuff).toEqual('-dba');

valueManager.update('test', 'super');
expect(et.props.other).toEqual('super-abc');
expect(et.props.stuff).toEqual('super-dba');

expect(et.props.when).toEqual('do-do');
});

});

0 comments on commit 3fa75d5

Please sign in to comment.