Skip to content

Instantly share code, notes, and snippets.

@miku
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save miku/8923224 to your computer and use it in GitHub Desktop.
Save miku/8923224 to your computer and use it in GitHub Desktop.
Progression. See also: http://imgur.com/a/Fdwxp
<!--
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>
<!--
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} &mdash;
<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}
&mdash; <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>
<!--
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