Last active
August 29, 2015 14:03
-
-
Save insin/a32a5f086d0028239044 to your computer and use it in GitHub Desktop.
React Questions - dumb-as-rocks, JSON-driven question sets / http://bl.ocks.org/insin/raw/a32a5f086d0028239044/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>React Questions</title> | |
<script src="http://fb.me/react-0.10.0.js"></script> | |
<script src="http://fb.me/JSXTransformer-0.10.0.js"></script> | |
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.5.0/pure-min.css"> | |
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.5.0/grids-responsive-min.css"> | |
<link rel="stylesheet" href="style.css"> | |
</head> | |
<body> | |
<div class="content"> | |
<div id="app"></div> | |
</div> | |
<script src="questionsets.js"></script> | |
<script type="text/jsx" src="questions.jsx"></script> | |
<a href="https://gist.github.com/insin/a32a5f086d0028239044"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @jsx React.DOM | |
*/ | |
// =================================================================== Utils === | |
function extend(dest) { | |
for (var i = 1; i < arguments.length; i++) { | |
var src = arguments[i] | |
if (src) { | |
for (var prop in src) { | |
if (Object.hasOwnProperty.call(src, prop)) { | |
dest[prop] = src[prop] | |
} | |
} | |
} | |
} | |
return dest | |
} | |
function inherits(sub, super_) { | |
sub.prototype = Object.create(super_.prototype) | |
sub.prototype.constructor = sub | |
return sub | |
} | |
// ======================================================== React Components === | |
function renderError(error) { | |
return error && <p className="error">{error}</p> | |
} | |
/** | |
* Simple component which displays a question code and a label. | |
*/ | |
var LabelView = React.createClass({ | |
render: function() { | |
return <div className="pure-control-group question question-label"> | |
<div className="pure-g"> | |
<div className="pure-u-1-12"><label><strong>{this.props.code}</strong></label></div> | |
<div className="pure-u-11-12"><label>{this.props.text}</label></div> | |
</div> | |
</div> | |
} | |
}) | |
/** | |
* Common properties and utility methods for Question components. | |
*/ | |
var QuestionMixin = { | |
propTypes: { | |
code: React.PropTypes.string.isRequired, | |
error: React.PropTypes.string, | |
handleResponse: React.PropTypes.func.isRequired, | |
prefix: React.PropTypes.string, | |
response: React.PropTypes.any, | |
text: React.PropTypes.string.isRequired | |
}, | |
/** | |
* Creates unique names. | |
*/ | |
getName: function(suffix) { | |
var name = this.props.code | |
if (this.props.prefix) { | |
name = this.props.prefix + '_' + name | |
} | |
if (suffix) { | |
name += "_" + suffix | |
} | |
return name | |
} | |
} | |
/** | |
* A question which provides Yes/No responses as radio buttons. | |
*/ | |
var YesNoQuestionView = React.createClass({ | |
mixins: [QuestionMixin], | |
handleChange: function(e) { | |
this.props.handleResponse(this.props.code, e.target.value) | |
}, | |
render: function() { | |
var name = this.getName() | |
return <div className="pure-control-group question"> | |
{renderError(this.props.error)} | |
<div className="pure-g"> | |
<div className="pure-u-1-12"><label><strong>{this.props.code}</strong></label></div> | |
<div className="pure-u-11-12"> | |
<label>{this.props.text}</label> | |
<label className="pure-radio"><input name={name} type="radio" value="Yes" onChange={this.handleChange} defaultChecked={this.props.response == 'Yes'}/> Yes</label> | |
<label className="pure-radio"><input name={name} type="radio" value="No" onChange={this.handleChange} defaultChecked={this.props.response == 'No'}/> No</label> | |
</div> | |
</div> | |
</div> | |
} | |
}) | |
/** | |
* A question which provides a free-form textarea. | |
*/ | |
var FreeformQuestionView = React.createClass({ | |
mixins: [QuestionMixin], | |
handleChange: function(e) { | |
this.props.handleResponse(this.props.code, e.target.value) | |
}, | |
render: function() { | |
var id = this.getName() | |
return <div className="pure-control-group question"> | |
{renderError(this.props.error)} | |
<div className="pure-g"> | |
<div className="pure-u-1-12"><label htmlFor={id}><strong>{this.props.code}</strong></label></div> | |
<div className="pure-u-11-12"> | |
<label htmlFor={id}>{this.props.text}</label> | |
<textarea id={id} onChange={this.handleChange} defaultValue={this.props.response || ''}/> | |
</div> | |
</div> | |
</div> | |
} | |
}) | |
/** | |
* A question which provides a selectable option. | |
*/ | |
var SelectableOptionView = React.createClass({ | |
mixins: [QuestionMixin], | |
render: function() { | |
var defaultChecked = (this.props.response === true) | |
return <div className="selectable-option"> | |
<div className="pure-g"> | |
<div className="pure-u-1-12"></div> | |
<div className="pure-u-11-12"> | |
<label className="pure-checkbox"><input type="checkbox" defaultChecked={defaultChecked} onChange={this.handleChange}/> {this.props.text}</label> | |
</div> | |
</div> | |
</div> | |
}, | |
handleChange: function(e) { | |
this.props.handleResponse(this.props.code, e.target.checked) | |
} | |
}) | |
/** | |
* Renders the questions in a QuestionSet object and manages updating of | |
* response state as they're entered and triggering of validation. | |
*/ | |
var QuestionSetView = React.createClass({ | |
propTypes: { | |
q: React.PropTypes.instanceOf(QuestionSet) | |
}, | |
validate: function() { | |
this.props.q.validate() | |
this.forceUpdate() | |
}, | |
handleResponse: function(code, response) { | |
this.props.q.responses[code] = response | |
this.props.q.validateOne(code) | |
this.forceUpdate() | |
}, | |
render: function() { | |
var q = this.props.q | |
var components = [] | |
q.questions.forEach(function(question) { | |
// Skip display of questions which have an unsatisfied askWhen condition | |
if (!question.shouldBeAsked(q.responses)) { | |
return | |
} | |
var component = question.render({ | |
key: question.code, | |
prefix: q.prefix, | |
response: q.responseFor(question), | |
error: q.errorFor(question), | |
handleResponse: this.handleResponse | |
}) | |
components.push(component) | |
}.bind(this)) | |
return <div> | |
{components} | |
<button type="button" onClick={this.validate} className="pure-button pure-button-primary">Validate</button> | |
<pre>Response JSON: {JSON.stringify(q.responses, null, 2)}</pre> | |
</div> | |
} | |
}) | |
/** | |
* Allows selection of the available Question Sets for previewing. | |
*/ | |
var App = React.createClass({ | |
getInitialState: function() { | |
return { | |
code: this.props.code || '' | |
} | |
}, | |
onChange: function(e) { | |
var code = e.target.value | |
if (code !== this.state.code) { | |
this.setState({code: e.target.value}) | |
} | |
}, | |
render: function() { | |
var q | |
if (this.state.code) { | |
var q = new QuestionSet(QUESTION_SETS[this.state.code]) | |
} | |
return <div> | |
<form className="pure-form pure-form-stacked"> | |
<div className="pure-g"> | |
<div className="pure-u-1-12"></div> | |
<div className="pure-u-11-12"> | |
<label htmlFor="questionSet">Question Set:</label> | |
<select id="questionSet" onChange={this.onChange} onKeyUp={this.onChange} defaultValue={this.state.code}><option></option>{this.renderSets()}</select> | |
</div> | |
</div> | |
{q && <div> | |
<h2>{q.name}</h2> | |
<QuestionSetView q={q}/> | |
</div>} | |
</form> | |
</div> | |
}, | |
renderSets: function() { | |
var codes = Object.keys(QUESTION_SETS) | |
codes.sort() | |
return codes.map(function(code) { | |
return <option value={code}>{code} - {QUESTION_SETS[code].name}</option> | |
}) | |
} | |
}) | |
// =================================================================== Model === | |
var TYPE_TO_MODEL = { | |
freeform: FreeformQuestion, | |
label: Label, | |
selectableoption: SelectableOption, | |
yesno: YesNoQuestion | |
} | |
function QuestionSet(props, responses) { | |
this.code = props.code | |
this.prefix = props.prefix | |
this.questions = props.questions.map(function(props) { | |
var model = TYPE_TO_MODEL[props.type] | |
if (!model) { | |
throw new Error('Unknown type: ' + JSON.stringify(props)) | |
} | |
return new model(props) | |
}) | |
this.responses = responses || {} | |
this.errors = {} | |
} | |
QuestionSet.prototype.validate = function() { | |
var errors = {} | |
var valid = true | |
this.questions.forEach(function(question) { | |
if (!question.shouldBeAsked(this.responses)) { | |
return | |
} | |
var response = this.responses[question.code] | |
var error = question.validate(response) | |
if (error) { | |
if (valid) { | |
valid = false | |
} | |
errors[question.code] = error | |
} | |
}.bind(this)) | |
this.errors = errors | |
if (valid) { | |
return this.responses | |
} | |
} | |
QuestionSet.prototype.validateOne = function(questionCode) { | |
// XXX Quick hack for clearing error status for a question if it has a | |
// respoonse. | |
if (this.responses[questionCode] && this.errors[questionCode]) { | |
delete this.errors[questionCode] | |
} | |
} | |
QuestionSet.prototype.responseFor = function(question) { | |
return this.responses[question.code] | |
} | |
QuestionSet.prototype.errorFor = function(question) { | |
return this.errors[question.code] | |
} | |
function Question(props) { | |
this.type = props.type | |
this.code = props.code | |
this.text = props.text | |
this.askWhen = props.askWhen | |
} | |
Question.prototype.shouldBeAsked = function(responses) { | |
if (this.askWhen) { | |
var parts = this.askWhen.split(' = ') | |
var code = parts[0] | |
var condition = parts[1] | |
if (responses[code] != condition) { | |
return false | |
} | |
} | |
return true | |
} | |
// All questions default to optional | |
Question.prototype.validate = function(response) {} | |
Question.prototype.render = function(props) { | |
var props = extend({}, this, props) | |
return this.view(props) | |
} | |
function Label(props) { | |
Question.call(this, props) | |
} | |
inherits(Label, Question) | |
Label.prototype.view = LabelView | |
function YesNoQuestion(props) { | |
Question.call(this, props) | |
} | |
inherits(YesNoQuestion, Question) | |
YesNoQuestion.prototype.view = YesNoQuestionView | |
YesNoQuestion.prototype.validate = function(response) { | |
if (!response) { | |
return 'Please provide a response' | |
} | |
} | |
function FreeformQuestion(props) { | |
Question.call(this, props) | |
} | |
inherits(FreeformQuestion, Question) | |
FreeformQuestion.prototype.view = FreeformQuestionView | |
function SelectableOption(props) { | |
Question.call(this, props) | |
} | |
inherits(SelectableOption, Question) | |
SelectableOption.prototype.view = SelectableOptionView | |
// ==================================================================== Init === | |
React.renderComponent(<App code="1"/>, document.getElementById('app')) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var QUESTION_SETS = { | |
"1": { | |
"code": "1", | |
"name": "Some Questions", | |
"questions": [ | |
{ | |
"text": "What is best in life?", | |
"code": "Q1", | |
"type": "label" | |
}, | |
{ | |
"text": "The open steppe", | |
"code": "Q1-1", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "Fleet horse", | |
"code": "Q1-2", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "Falcons at your wrist", | |
"code": "Q1-3", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "The wind in your hair.", | |
"code": "Q1-4", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "To crush your enemies", | |
"code": "Q1-5", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "To hear the lamentation of their women", | |
"code": "Q1-6", | |
"type": "selectableoption" | |
}, | |
{ | |
"text": "Other:", | |
"code": "Q1.1", | |
"type": "freeform" | |
}, | |
{ | |
"text": "Stop. Who would cross the Bridge of Death must answer me these questions three, ere the other side he see. Would you cross the Bridge of Death?", | |
"code": "Q2", | |
"type": "yesno" | |
}, | |
{ | |
"text": "What... is your name?", | |
"code": "Q2.1", | |
"type": "freeform", | |
"askWhen": "Q2 = Yes" | |
}, | |
{ | |
"text": "What... is your quest?", | |
"code": "Q2.2", | |
"type": "freeform", | |
"askWhen": "Q2 = Yes" | |
}, | |
{ | |
"text": "What... is your favourite colour?", | |
"code": "Q2.3", | |
"type": "freeform", | |
"askWhen": "Q2 = Yes" | |
}, | |
{ | |
"text": "About your knowledge of witches:", | |
"code": "Q3", | |
"type": "label" | |
}, | |
{ | |
"text": "Tell me. What do you do with witches?", | |
"code": "Q3.1", | |
"type": "freeform" | |
}, | |
{ | |
"text": "And what do you burn, apart from witches?", | |
"code": "Q3.2", | |
"type": "freeform" | |
}, | |
{ | |
"text": "Now, why do witches burn?", | |
"code": "Q3.3", | |
"type": "freeform" | |
}, | |
] | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
textarea { width: 100%; box-sizing: border-box; } | |
.error { color: #f00; } | |
.content { max-width: 800px; margin: 0 auto; } | |
.question { margin-bottom: 1.5em; } | |
.question-label { margin-top: 1.5em; margin-bottom: 0; } | |
.selectable-option + .question { margin-top: 1.5em; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment