Last active
August 29, 2015 13:56
-
-
Save miku/8923224 to your computer and use it in GitHub Desktop.
Progression. See also: http://imgur.com/a/Fdwxp
This file contains 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
<!-- | |
Visual comparison: http://imgur.com/a/Fdwxp | |
* Basic display. | |
* No multiple tags, multiple subfield values. | |
* No "Show more". | |
* 25 LOC. | |
--> | |
<script type="text/javascript"> | |
$(document).ready(function() { | |
var firstvalue = function(data, tag) { | |
var field = tag.substring(0, 3); | |
var subfield = tag.substring(3, 4); | |
try { value = data['content'][field][0][subfield] || "NA"; | |
} catch (err) { value = "NA"} | |
return value; | |
} | |
$("td.tdoc").each(function() { | |
var self = $(this); | |
var index = self.attr('index'); | |
var id = self.attr('id'); | |
var key = index + "-" + id; | |
$.get("/doc/" + index + "/" + id, function(data) { | |
fields = ['245a', '245b', '245c', '260c', '100a', '700a', | |
'655a', '935b', '020a', '300a', '500a']; | |
for (var i = 0; i < fields.length; i++) { | |
field = fields[i]; | |
$("#" + key + "-" + field).text(firstvalue(data, field)); | |
} | |
}); | |
}); | |
}); | |
</script> |
This file contains 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
<!-- | |
Visual comparison: http://imgur.com/a/Fdwxp | |
* Has two views (collapsed by default or `expanded` - state in <ComparisonTable>). | |
* Collapsed view can be configured via `defaultTags` property on <ComparisonTable> | |
* Field values autocollapses if they are longer than `cutoff` chars (see <FieldValue>) | |
* URLs are turned into links, which can be shortened to hostname via `shortenUrls` on <FieldValue> | |
* Handles multiple fields with the same tag and multiple values for the same subfield code. | |
* Loading indicator and rudimentary connection error handling. | |
* 250 LOC. | |
--> | |
<script type="text/jsx"> | |
/** @jsx React.DOM */ | |
var HyperLinkValue = React.createClass({ | |
getDefaultProps: function() { | |
return {'shortenUrls': false}; | |
}, | |
render: function() { | |
var value = this.props.value; | |
if (this.props.shortenUrls) { | |
return (<a href={value} title={value}> | |
{$.url(value).attr('host')}</a>); | |
} else { | |
return (<a href={value}>{value}</a>); | |
} | |
} | |
}); | |
var FieldValue = React.createClass({ | |
getInitialState: function() { | |
return {'collapsed': true}; | |
}, | |
getDefaultProps: function() { | |
return {'shortenUrls': true, | |
'cutoff': 75, | |
'id': Math.random().toString(36).slice(2) }; | |
}, | |
handleClick: function() { | |
this.setState({collapsed: !this.state.collapsed}); | |
this.preventDefault(); | |
}, | |
render: function() { | |
if (this.props.value === undefined) { | |
return (<td className="value not-available">-</td>); | |
} | |
if (_.str.startsWith(this.props.value, "http")) { | |
return (<td className="value"> | |
<HyperLinkValue value={this.props.value} | |
shortenUrls={this.props.shortenUrls} /></td>); | |
} else { | |
if (this.props.value.length > this.props.cutoff) { | |
if (this.state.collapsed) { | |
var value = _.str.prune(this.props.value, | |
this.props.cutoff, ""); | |
var remainingChars = this.props.value.length - | |
this.props.cutoff; | |
return (<td className="value">{value} — | |
<a onClick={this.handleClick} | |
href={"#" + this.props.id}> | |
<span className="expand-value"> | |
{remainingChars} more...</span></a></td>); | |
} else { | |
return (<td className="value">{this.props.value} | |
— <a onClick={this.handleClick} | |
href={"#" + this.props.id}>Collapse</a></td>); | |
} | |
} else { | |
return (<td className="value">{this.props.value}</td>); | |
} | |
} | |
} | |
}); | |
var FieldRow = React.createClass({ | |
getDefaultProps: function() { | |
omitTag: true | |
}, | |
render: function() { | |
var tag = this.props.tag; | |
if (this.props.omitTag) { | |
tag = ""; | |
} | |
var rowIsComparable = (this.props.leftValue !== undefined && | |
this.props.rightValue !== undefined); | |
var comparableClass = rowIsComparable ? "comparable" : "uncomparable"; | |
return (<tr className="field" className={comparableClass}> | |
<td className="tag">{tag}</td> | |
<td>{this.props.code}</td> | |
<FieldValue value={this.props.leftValue} /> | |
<FieldValue value={this.props.rightValue} /> | |
</tr>); | |
} | |
}); | |
var ComparisonTable = React.createClass({ | |
getDefaultProps: function() { | |
return {defaultTags: {"001": [], | |
"020": ["a", "9"], | |
"100": [], | |
"245": ["a", "b", "c"], | |
"260": ["a", "b", "c"], | |
"300": ["a"], | |
"500": ["a"], | |
"655": ["a"], | |
"700": ["a"], | |
"935": ["a", "b"]}}; | |
}, | |
getInitialState: function() { | |
return {expanded: false}; | |
}, | |
getAllDefinedTags: function() { | |
var keys = _.union(Object.keys(this.props.left['record']['content']), | |
Object.keys(this.props.right['record']['content'])); | |
dict = {} | |
keys.forEach(function(key) { dict[key] = []; }); | |
return dict; | |
}, | |
handleClick: function() { | |
this.setState({expanded: !this.state.expanded}); | |
}, | |
render: function() { | |
$this = this; | |
var rows = []; | |
var tags = this.state.expanded ? this.getAllDefinedTags() : | |
this.props.defaultTags; | |
Object.keys(tags).sort().forEach(function(tag, i) { | |
leftValue = $this.props.left['record']['content'][tag] || | |
(tag < "010" ? "-" : []); | |
rightValue = $this.props.right['record']['content'][tag] || | |
(tag < "010" ? "-" : []); | |
if (tag < "010") { | |
// simple control field | |
rows.push(<FieldRow tag={tag} omitTag={false} code="" | |
leftValue={leftValue} rightValue={rightValue} />); | |
} else { | |
// complicated data field | |
// maximum number of repeated fields, e.g. 2 x "020" etc. | |
var max = Math.max(leftValue.length, rightValue.length); | |
for (var i = 0; i < max; i++) { | |
leftValue[i] = leftValue[i] || {}; | |
rightValue[i] = rightValue[i] || {}; | |
// subfield codes | |
leftCodes = Object.keys(leftValue[i]); | |
rightCodes = Object.keys(rightValue[i]); | |
var codes = _.union(leftCodes, rightCodes); | |
// ignore indicator for now | |
codes = _.without(codes, "ind1", "ind2"); | |
// for each subfield code that appears in either doc | |
for (var j = 0; j < codes.length; j++) { | |
var code = codes[j]; | |
if (tags[tag].length == 0 || | |
_.contains(tags[tag], code)) { | |
// get the subfield value ... | |
var leftSubfieldValue = leftValue[i][code]; | |
var rightSubfieldValue = rightValue[i][code]; | |
// .. normalize to an array | |
// TODO: make arrays the default down the chain! | |
if (!_.isArray(leftSubfieldValue)) { | |
leftSubfieldValue = [leftSubfieldValue]; | |
} | |
if (!_.isArray(rightSubfieldValue)) { | |
rightSubfieldValue = [rightSubfieldValue]; | |
} | |
// typical subfield value count is 1, | |
// but repeated subfield values have been | |
// spotted in 100.a, 936.k, ... | |
maxSubfieldValueCount = Math.max( | |
leftSubfieldValue.length, | |
rightSubfieldValue.length); | |
for (var k = 0; k < maxSubfieldValueCount; k++) { | |
rows.push(<FieldRow tag={tag} | |
omitTag={k > 0 || j > 0} | |
code={code} | |
leftValue={leftSubfieldValue[k]} | |
rightValue={rightSubfieldValue[k]} />); | |
} | |
} | |
} | |
} | |
} | |
}); | |
var linkText = this.state.expanded ? "Fewer Details" : "More Details"; | |
rows.push(<tr> | |
<td colSpan="4"><a onClick={this.handleClick} | |
href="#">{linkText}</a> </td></tr>); | |
return (<table><tbody>{rows}</tbody></table>); | |
} | |
}); | |
var Comparison = React.createClass({ | |
getInitialState: function() { | |
return {left: {}, right: {}, }; | |
}, | |
getDefaultProps: function() { | |
return {'base': 'http://localhost:5000/doc'} | |
}, | |
componentWillMount: function() { | |
var $this = this; | |
var leftParts = this.props.left.split("://"); | |
var leftIndex = leftParts[0]; | |
var leftId = leftParts[1]; | |
var leftUrl = this.props.base + "/" + leftIndex + "/" + leftId; | |
var rightParts = this.props.right.split("://"); | |
var rightIndex = rightParts[0]; | |
var rightId = rightParts[1]; | |
var rightUrl = this.props.base + "/" + rightIndex + "/" + rightId; | |
$.ajax({ | |
url: leftUrl, | |
datatype: 'json', | |
success: function(data) { | |
$this.setState({left: {index: leftIndex, id: leftId, | |
record: data}}); | |
}, | |
error: function(xhr, status, err) { | |
console.error(err); | |
} | |
}); | |
$.ajax({ | |
url: rightUrl, | |
datatype: 'json', | |
success: function(data) { | |
$this.setState({right: {index: rightIndex, id: rightId, | |
record: data}}); | |
}, | |
error: function(xhr, status, err) { | |
console.error(err); | |
} | |
}); | |
}, | |
render: function() { | |
if (_.isEmpty(this.state.left) || _.isEmpty(this.state.right)) { | |
return (<div><p>Loading...</p></div>); | |
} else { | |
return (<div><ComparisonTable left={this.state.left} | |
right={this.state.right} /></div>); | |
} | |
} | |
}); | |
React.renderComponent( | |
<Comparison base="http://0.0.0.0:5000/doc" | |
left="{{ payload.left.index }}://{{ payload.left.id }}" | |
right="{{ payload.right.index }}://{{ payload.right.id }}" />, | |
document.getElementById("comparison")); | |
</script> |
This file contains 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
<!-- | |
Just here as a reminder for many mistakes that are possible - First mainly wrong attempt. | |
--> | |
<script type="text/jsx"> | |
/** @jsx React.DOM */ | |
var CTableRow = React.createClass({ | |
render: function() { | |
return ( | |
<tr> | |
<td>{this.props.row[0]}</td> | |
<td>{this.props.row[1]}</td> | |
<td>{this.props.row[2]}</td> | |
</tr> | |
); | |
} | |
}); | |
var CTable = React.createClass({ | |
getInitialState: function() { | |
// FAIL: no props in state! (http://goo.gl/OaufW7) | |
return {rows: this.props.rows} | |
}, | |
render: function() { | |
return ( | |
<table> | |
{this.props.rows.map(function(row) { | |
return <CTableRow row={row} /> | |
})} | |
</table> | |
); | |
} | |
}); | |
var Comparison = React.createClass({ | |
getInitialState: function() { | |
return { | |
docs: [], | |
// FAIL: everything below can be computed from the actual docs; not a state | |
keys: [], | |
keysIntersection: [], | |
keysUnion: [], | |
rows: [], | |
}; | |
}, | |
// Given an {"index": "name", "id": "1234"} descriptor, retrieve the | |
// document from ES/server and append it to this.state.docs array | |
loadDocs: function(descriptor) { | |
return $.ajax({ | |
url: '/doc/' + descriptor.index + "/" + descriptor.id, | |
dataType: 'json', | |
success: function(data) { | |
var current = this.state.docs; | |
current.push(data); | |
this.setState({docs: current}); | |
}.bind(this), | |
error: function(xhr, status, err) { | |
console.error("failed to load document", status, err.toString()); | |
console.error(descriptor); | |
}.bind(this) | |
}); | |
}, | |
// Get the documents from ES, extract the tags and cache | |
// intersection and union of the tags | |
componentWillMount: function() { | |
var $this = this; | |
var promises = this.props.docs.map(function(d) { | |
return $this.loadDocs(d) | |
}); | |
$.when.apply($, promises).done(function() { | |
for (var i = 0; i < $this.state.docs.length; i++) { | |
var current = $this.state.keys; | |
current.push(Object.keys($this.state.docs[i]["content"])); | |
$this.setState({keys: current}); | |
} | |
// FAIL AGAIN: keysXXX is not a state! | |
$this.setState({keysIntersection: _.intersection.apply(_, $this.state.keys).sort() }); | |
$this.setState({keysUnion: _.union.apply(_, $this.state.keys).sort() }); | |
console.log($this.state); | |
// extract the table rows | |
for (var i = 0; i < $this.state.keysIntersection.length; i++) { | |
var key = $this.state.keysIntersection[i]; | |
var values = []; | |
values.push(key); | |
for (var j = 0; j < $this.state.docs.length; j++) { | |
var value = $this.state.docs[j]["content"][key]; | |
if (_.isString(value)) { | |
values.push(value) | |
} else { | |
// TODO: make this better :) | |
//// NOT FAILED :) | |
values.push(value[0]); | |
} | |
} | |
// FAIL: only the docs are state, the rows can be computed | |
var current = $this.state.rows; | |
current.push(values); | |
$this.setState({rows: current}); | |
} | |
console.log($this.state); | |
}).fail(function(xhr, status, err) { | |
console.error("failed to mount component", status, err.toString()); | |
console.error($this); | |
}); | |
}, | |
render: function() { | |
console.log("rendering..."); | |
// FAIL, because we rely on when().then() to handle state, whereas | |
// it's better done like this: https://gist.github.com/miku/8923224#file-2-react-html-L245 | |
return ( | |
<div> | |
{} | |
<CTable rows={this.state.rows} /> | |
</div> | |
); | |
} | |
}) | |
React.renderComponent( | |
<Comparison docs={[{"index": "bsz", "id": "310883989"}, | |
{"index": "nep", "id": "9780415474825"}]} />, | |
document.getElementById('example') | |
); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment