<!DOCTYPE html>
<html>
<head>
<script data-require="react@*" data-semver="0.12.2" src="//cdnjs.cloudflare.com/ajax/libs/react/0.12.2/react.js"></script>
<script src="https://rawgit.com/rsamec/react-binding/master/dist/react-binding.min.js"></script>
<script src="https://rawgit.com/flesler/hashmap/master/hashmap.js"></script>
<script src="https://rawgit.com/rsamec/business-rules-engine/master/dist/business-rules-engine.js"></script>
<script data-require="underscore.js@1.6.0" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="content"></div>
<script src="script.js"></script>
</body>
</html>
var Binder = Binder.default;
var Hobby = React.createClass({
removeHobby: function(e){
return this.props.onDelete(this.props.model.value);
},
frequencyName:function(){ return "frequency" + this.props.index;},
hobbyLabel:function(){ return (this.props.index + 1) + ". hobby name";},
render: function() {
return (
<div className="comment">
<TextBoxInput label={this.hobbyLabel()} model={Binder.bindTo(this.props.model,"HobbyName")} error={this.props.error.HobbyName} />
<button onClick={this.removeHobby}>Delete</button>
<RadioGroup name={this.frequencyName()}
valueLink={Binder.bindTo(this.props.model,"Frequency")}>
<div>
<label>
<input type="radio" value="Daily" />Daily
</label>
<label>
<input type="radio" value="Weekly" />Weekly
</label>
<label>
<input type="radio" value="Monthly" />Monthly
</label>
</div>
</RadioGroup>
<CheckBoxInput label="Is this a paid hobby?" model={Binder.bindTo(this.props.model,"Paid")} />
<CheckBoxInput label="Would you recommend this hobby to a friend?" model={Binder.bindTo(this.props.model,"Recommendation")} />
</div>
);
}
});
var HobbyForm = React.createClass({
getInitialState: function() {
return {
data: {},
rules:new FormSchema.JsonSchemaRuleFactory(BusinessRules).CreateRule("Main")
};
},
addHobby:function(e){
if (this.state.data.Hobbies === undefined)
this.state.data.Hobbies = []
this.state.data.Hobbies.push({});
this.setState({data:this.state.data})
},
result:function(){
if (this.state.rules === undefined) return {Errors:{}};
return Utils.CompositeDotObject.Transform(this.state.rules.Validate(this.state.data)).Main;
},
render: function() {
return (
<div className="commentBox">
<h1>Hobbies form</h1>
<div style={{"float":"right","width":"400"}}>
<PrettyJson json={this.state.data} />
</div>
<div >
<PersonComponent model={Binder.bindToState(this,"data","Person")} error={this.result().Person} />
<button value="Add" onClick={this.addHobby}>Add</button>
<div className="error">{this.result().Hobbies.PropRule.ErrorMessage}</div>
<HobbyList model={Binder.bindArrayToState(this,"data","Hobbies")} errors={this.result().Hobbies.Children} />
</div>
</div>
);
}
});
var HobbyList = React.createClass({
handleDelete: function(hobby){
return this.props.model.remove(hobby);
},
render: function() {
if (this.props.model.items === undefined) return <span>There are no hobbies.</span>;
var hobbyNodes = this.props.model.items.map(function(hobby, index) {
return (
<Hobby model={hobby} key={index} index={index} onDelete={this.handleDelete} error={this.props.errors[index]} />
);
},this);
return (
<div className="commentList">
{hobbyNodes}
</div>
);
}
});
var PersonComponent = React.createClass({
render: function() {
return (
<div>
<TextBoxInput label="First name" model={Binder.bindTo(this.props.model,"FirstName")} error={this.props.error.FirstName} />
<TextBoxInput label="Last name" model={Binder.bindTo(this.props.model,"LastName")} error={this.props.error.LastName} />
<TextBoxInput label="Email" model={Binder.bindTo(this.props.model,"Contact.Email")} error={this.props.error.Contact.Email} />
</div>
);
}
});
var TextBoxInput = React.createClass({
render: function() {
return (
<div style={{display:'inline'}}>
<label style={{display:'block'}}>{this.props.label}</label>
<input type='text' valueLink={this.props.model} />
<div className="error">{this.props.error.ErrorMessage}</div>
</div>
)
}
});
var CheckBoxInput = React.createClass({
render: function() {
var valueModel = this.props.model;
var handleChange = function(e){
valueModel.value = e.target.checked;
}
return (
<label>
<input type='checkbox' onChange={handleChange} checked={valueModel.value} />
{this.props.label}
</label>
)
}
});
var PrettyJson = React.createClass({
replacer: function (match, pIndent, pKey, pVal, pEnd) {
var key = '<span class=json-key>';
var val = '<span class=json-value>';
var str = '<span class=json-string>';
var r = pIndent || '';
if (pKey)
r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
if (pVal)
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
return r + (pEnd || '');
},
prettyPrint: function (obj) {
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
return JSON.stringify(obj, null, 3)
.replace(/&/g, '&').replace(/\\"/g, '"')
.replace(/</g, '<').replace(/>/g, '>')
.replace(jsonLine, this.replacer);
},
render: function () {
return (<pre dangerouslySetInnerHTML={{__html: this.prettyPrint(this.props.json)}}></pre>);
}
})
var RadioGroup = React.createClass({displayName: 'RadioGroup',
getInitialState: function() {
// check the first block of comment in `setCheckedRadio`
return {defaultValue: this.props.defaultValue};
},
componentDidMount: function() {
this.setRadioNames();
this.setCheckedRadio();
},
componentDidUpdate: function() {
this.setRadioNames();
this.setCheckedRadio();
},
render: function() {
return (
React.DOM.div({ onChange:this.props.onChange },
this.props.children
)
);
},
setRadioNames: function() {
// stay DRY and don't put the same `name` on all radios manually. Put it on
// the tag and it'll be done here
var $radios = this.getRadios();
for (var i = 0, length = $radios.length; i < length; i++) {
$radios[i].setAttribute('name', this.props.name);
}
},
getRadios: function() {
return this.getDOMNode().querySelectorAll('input[type="radio"]');
},
setCheckedRadio: function() {
var $radios = this.getRadios();
// if `value` is passed from parent, always use that value. This is similar
// to React's controlled component. If `defaultValue` is used instead,
// subsequent updates to defaultValue are ignored. Note: when `defaultValue`
// and `value` are both passed, the latter takes precedence, just like in
// a controlled component
var destinationValue = this.props.value != null
? this.props.value
: this.state.defaultValue;
for (var i = 0, length = $radios.length; i < length; i++) {
var $radio = $radios[i];
// intentionally use implicit conversion for those who accidentally used,
// say, `valueToChange` of 1 (integer) to compare it with `value` of "1"
// (auto conversion to valid html value from React)
if ($radio.value == destinationValue) {
$radio.checked = true;
}
}
},
getCheckedValue: function() {
var $radios = this.getRadios();
for (var i = 0, length = $radios.length; i < length; i++) {
if ($radios[i].checked) {
return $radios[i].value;
}
}
return null;
}
});
var BusinessRules = {
"Person": {
"type": "object",
"properties": {
"FirstName": {
"type": "string",
"title": "First name",
"required": "true",
"maxLength": "15"
},
"LastName": {
"type": "string",
"title": "Last name",
"required": "true",
"maxLength": "15"
},
"Contact": {
"type": "object",
"properties": {
"Email": {
"type": "string",
"title": "Email",
"required": "true",
"maxLength": 100,
"email": "true"
}
}
}
}
},
"Hobbies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"HobbyName": {
"type": "string",
"title": "HobbyName",
"required": "true",
"maxLength": 100
},
"Frequency": {
"type": "string",
"title": "Frequency",
"enum": ["Daily", "Weekly", "Monthly"]
},
"Paid": {
"type": "boolean",
"title": "Paid"
},
"Recommedation": {
"type": "boolean",
"title": "Recommedation"
}
}
},
"maxItems": 4,
"minItems": 2
}
};
React.render(
<HobbyForm />,
document.getElementById('content')
);
/* Styles go here */
.error{
color: darkred;
}